Merge "Add Typescript test migration instruction to README.md"
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 3e95e42..2266ba0 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -1,12 +1,5 @@
# Gerrit Polymer Frontend
-**Warning**: DON'T ADD MORE TYPESCRIPT FILES/TYPES. Gerrit Polymer Frontend
-contains several typescript files and uses typescript compiler. This is a
-preparation for the upcoming migration to typescript and we actively working on
-it. We want to avoid massive typescript-related changes until the preparation
-work is done. Thanks for your understanding!
-
-
Follow the
[setup instructions for Gerrit backend developers](https://gerrit-review.googlesource.com/Documentation/dev-readme.html)
where applicable, the most important command is:
@@ -279,6 +272,245 @@
npm run polylint
```
+## Migrating tests to Typescript
+
+You can use the following steps for migrating tests to Typescript:
+
+1. Rename the `_test.js` file to `_test.ts`
+2. Remove `.js` extensions from all imports:
+ ```
+ // Before:
+ import ... from 'x/y/z.js`
+
+ // After
+ import .. from 'x/y/z'
+ ```
+3. Fix typescript and eslint errors.
+
+Common errors and fixes are:
+
+* An object in the test doesn't have all required properties. You can use
+existing helpers to create an object with all required properties:
+```
+// Before:
+sinon.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+
+// After:
+Promise.resolve({
+ ...createPreferences(),
+ default_diff_view: DiffViewMode.UNIFIED,
+})
+```
+
+Some helpers receive parameters:
+```
+// Before
+element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1, commit: {parents: []}},
+ rev2: {_number: 2, commit: {parents: []}},
+ },
+ current_revision: 'rev1',
+ status: ChangeStatus.MERGED,
+ labels: {},
+ actions: {},
+};
+
+// After
+element._change = {
+ ...createChange(),
+ // The change_id is set by createChange.
+ // The exact change_id is not important in the test, so it was removed.
+ revisions: {
+ rev1: createRevision(1), // _number is a parameter here
+ rev2: createRevision(2), // _number is a parameter here
+ },
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.MERGED,
+ labels: {},
+ actions: {},
+};
+```
+* Typescript reports some weird messages about `window` property - sometimes an
+IDE adds wrong import. Just remove it.
+```
+// The wrong import added by IDE, must be removed
+import window = Mocha.reporters.Base.window;
+```
+
+* `TS2531: Object is possibly 'null'`. To fix use either non-null assertion
+operator `!` or nullish coalescing operator `?.`:
+```
+// Before:
+const rows = element
+ .shadowRoot.querySelector('table')
+ .querySelectorAll('tbody tr');
+...
+// The _robotCommentThreads declared as _robotCommentThreads?: CommentThread
+assert.equal(element._robotCommentThreads.length, 2);
+
+// Fix with non-null assertion operator:
+const rows = element
+ .shadowRoot!.querySelector('table')! // '!' after shadowRoot and querySelector
+ .querySelectorAll('tbody tr');
+
+assert.equal(element._robotCommentThreads!.length, 2);
+
+// Fix with nullish coalescing operator:
+ assert.equal(element._robotCommentThreads?.length, 2);
+```
+Usually the fix with `!` is preferable, because it gives more clear error
+when an intermediate property is `null/undefined`. If the _robotComments is
+`undefined` in the example above, the `element._robotCommentThreads!.length`
+crashes with the error `Cannot read property 'length' of undefined`. At the
+same time the fix with
+`?.` doesn't distinct between 2 cases: _robotCommentThreads is `undefined`
+and `length` is `undefined`.
+
+* `TS2339: Property '...' does not exist on type 'Element'.` for elements
+returned by `querySelector/querySelectorAll`. To fix it, use generic versions
+of those methods:
+```
+// Before:
+const radios = parentTable
+ .querySelectorAll('input[type=radio]');
+const radio = parentRow
+ .querySelector('input[type=radio]');
+
+// After:
+const radios = parentTable
+ .querySelectorAll<HTMLInputElement>('input[type=radio]');
+const radio = parentRow
+ .querySelector<HTMLInputElement>('input[type=radio]');
+```
+
+* Sinon: `TS2339: Property 'lastCall' does not exist on type '...` (the same
+for other sinon properties). Store stub/spy in a variable and then use the
+variable:
+```
+// Before:
+sinon.stub(GerritNav, 'getUrlForChange')
+...
+assert.equal(GerritNav.getUrlForChange.lastCall.args[4], '#message-a12345');
+
+// After:
+const getUrlStub = sinon.stub(GerritNav, 'getUrlForChange');
+...
+assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+```
+
+If you need to define a type for such variable, you can use one of the following
+options:
+```
+suite('my suite', () => {
+ // Non static members, option 1
+ let updateHeightSpy: SinonSpyMember<typeof element._updateRelatedChangeMaxHeight>;
+ // Non static members, option 2
+ let updateHeightSpy_prototype: SinonSpyMember<typeof GrChangeView.prototype._updateRelatedChangeMaxHeight>;
+ // Static members
+ let navigateToChangeStub: SinonStubbedMember<typeof GerritNav.navigateToChange>;
+ // For interfaces
+ let getMergeableStub: SinonStubbedMember<RestApiService['getMergeable']>;
+});
+```
+
+* Typescript reports errors when stubbing/faking methods:
+```
+// The JS code:
+const reloadStub = sinon
+ .stub(element, '_reload')
+ .callsFake(() => Promise.resolve());
+
+stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ _fetchSharedCacheURL() { return Promise.resolve({}); },
+});
+```
+
+In such cases, validate the input and output of a stub/fake method. Quite often
+tests return null instead of undefined or `[]` instead of `{}`, etc...
+Fix types if they are not correct:
+```
+const reloadStub = sinon
+ .stub(element, '_reload')
+ // GrChangeView._reload method returns an array
+ .callsFake(() => Promise.resolve([])); // return [] here
+
+stub('gr-rest-api-interface', {
+ ...
+ // Fix return type:
+ _fetchSharedCacheURL() { return Promise.resolve({} as ParsedJSON); },
+});
+```
+
+If a method has multiple overloads, you can use one of 2 options:
+```
+// Option 1: less accurate, but shorter:
+function getCommentsStub() {
+ return Promise.resolve({});
+}
+
+stub('gr-rest-api-interface', {
+ ...
+ getDiffComments: (getCommentsStub as unknown) as RestApiService['getDiffComments'],
+ getDiffRobotComments: (getCommentsStub as unknown) as RestApiService['getDiffRobotComments'],
+ getDiffDrafts: (getCommentsStub as unknown) as RestApiService['getDiffDrafts'],
+ ...
+});
+
+// Option 2: more accurate, but longer.
+// Step 1: define the same overloads for stub:
+function getDiffCommentsStub(
+ changeNum: NumericChangeId
+): Promise<PathToCommentsInfoMap | undefined>;
+function getDiffCommentsStub(
+ changeNum: NumericChangeId,
+ basePatchNum: PatchSetNum,
+ patchNum: PatchSetNum,
+ path: string
+): Promise<GetDiffCommentsOutput>;
+
+// Step 2: implement stub method for differnt input
+function getDiffCommentsStub(
+ _: NumericChangeId,
+ basePatchNum?: PatchSetNum,
+):
+ | Promise<PathToCommentsInfoMap | undefined>
+ | Promise<GetDiffCommentsOutput> {
+ if (basePatchNum) {
+ return Promise.resolve({
+ baseComments: [],
+ comments: [],
+ });
+ }
+ return Promise.resolve({});
+}
+
+// Step 3: use stubbed function:
+stub('gr-rest-api-interface', {
+ ...
+ getDiffComments: getDiffCommentsStub,
+ ...
+});
+```
+
+* If a test requires a `@types/...` library, install the required library
+in the `polygerrit_ui/node_modules` and update the `typeRoots` in the
+`polygerrit-ui/app/tsconfig_bazel_test.json` file.
+
+The same update should be done if a test requires a .d.ts file from a library
+that already exists in `polygerrit_ui/node_modules`.
+
+**Note:** Types from a library located in `polygerrit_ui/app/node_modules` are
+handle automatically.
+
+* If a test imports a library from `polygerrit_ui/node_modules` - update
+`paths` in `polygerrit-ui/app/tsconfig_bazel_test.json`.
+
## Contributing
Our users report bugs / feature requests related to the UI through [Monorail Issues - PolyGerrit](https://bugs.chromium.org/p/gerrit/issues/list?q=component%3APolyGerrit).