Skip to content

Commit 47b3ccb

Browse files
browser: Get Browser Unit Tests Working (#4255)
* browser: update webpack file for browser - Makes minor changes to our browser webpack related functionality. - remove 'fs-extra' dependency Signed-off-by: nkomonen <nkomonen@amazon.com> * browser: change how globals is set with globalThis See documentation for explanation. Signed-off-by: nkomonen <nkomonen@amazon.com> * comments + clean up Signed-off-by: nkomonen <nkomonen@amazon.com> * minor fixes Signed-off-by: nkomonen <nkomonen@amazon.com> --------- Signed-off-by: nkomonen <nkomonen@amazon.com>
1 parent 6e974a8 commit 47b3ccb

File tree

11 files changed

+122
-46
lines changed

11 files changed

+122
-46
lines changed

.vscode/launch.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"request": "launch",
3232
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web"],
3333
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
34-
"preLaunchTask": "npm: buildBrowser"
34+
"preLaunchTask": "buildBrowserDebug"
3535
},
3636
{
3737
"name": "Extension (webpack)",
@@ -85,14 +85,13 @@
8585
"debugWebWorkerHost": true,
8686
"request": "launch",
8787
"args": [
88-
"--disable-extensions",
8988
"--extensionDevelopmentPath=${workspaceFolder}",
9089
"--extensionDevelopmentKind=web",
9190
"--extensionTestsPath=${workspaceFolder}/dist/src/testBrowser/testRunner",
9291
"${workspaceRoot}/dist/src/testFixtures/workspaceFolder"
9392
],
9493
"outFiles": ["${workspaceFolder}/dist/src/**/*.js"],
95-
"preLaunchTask": "npm: buildBrowser"
94+
"preLaunchTask": "buildBrowserDebug"
9695
},
9796
{
9897
"name": "Integration Tests",

.vscode/tasks.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,10 @@
3737
}
3838
},
3939
{
40-
"label": "npm: buildBrowser",
40+
"label": "buildBrowserDebug",
4141
"type": "npm",
42-
"script": "buildBrowser",
43-
"dependsOn": ["watch"],
44-
"detail": "Builds the webpacked browser bundle"
42+
"script": "buildBrowserDebug",
43+
"detail": "Builds the webpacked browser bundle for debugging"
4544
},
4645
{
4746
"type": "npm",

docs/browser.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,64 @@ to help us visualize the imports and determine which module is importing a certa
7878
![Dependency Graph](./images/dependency-graph.svg)
7979
- Additionally specify a certain dependency with `--reaches` , `npx depcruise src/srcShared/fs.ts --reaches "fs-extra" --output-type dot | dot -T svg > dependency-graph.svg`, to hide unrelated dependencies:
8080
![Dependency Graph](./images/dependency-graph-small.svg)
81+
82+
## Global Scoped Objects + Testing behavior
83+
84+
### Summary
85+
86+
- **When in Browser mode**, state is not shared between the actual extension code and the unit test code. I.e you cannot modify a global variable in the extension code and see that change in the unit tests
87+
- State will need to be stored somewhere in `globalThis` to be accessible by tests. Any state not in `globalThis` will not be the same as the actual extension, they are separate.
88+
89+
With the introduction of Browser support, it was discovered that the context between the extension code and test code is not shared.
90+
Though it is shared in the Node version of the extension.
91+
92+
Example that does NOT work in the Browser:
93+
94+
```typescript
95+
export let myGlobal = 'A'
96+
97+
function activate() {
98+
// Change the global variable value
99+
myGlobal = 'B'
100+
}
101+
```
102+
103+
```typescript
104+
// Browser unit test
105+
import { myGlobal } from '../src/extension.ts'
106+
107+
describe('test', function () {
108+
it('test', function () {
109+
assert.strictEqual(myGlobal, 'B') // this fails in Browser (but not Node.js). The value here is actually 'A'.
110+
})
111+
})
112+
```
113+
114+
Example that DOES work in the Browser:
115+
116+
```typescript
117+
;(globalThis as any).myGlobal = 'A'
118+
119+
function activate() {
120+
;(globalThis as any).myGlobal = 'B'
121+
}
122+
```
123+
124+
```typescript
125+
// Browser unit test
126+
describe('test', function () {
127+
it('test', function () {
128+
assert.strictEqual((globalThis as any).myGlobal, 'B') // this succeeds in Browser and Node.js
129+
})
130+
})
131+
```
132+
133+
### Web Worker
134+
135+
The assumption for the behavior is due to how Web Workers work. We (VS Code) use them to run our extension and unit test code when in the Browser. The scripts share global values differently compared to a different environment such as Node.js.
136+
137+
- [`WorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope)
138+
- The context of the executing code is contained within itself and is not accessible to other scripts (eg: extension code context is not accessible by unit tests)
139+
- VS Code uses Dedicated Workers since `globalThis` is indicated as a [`DedicatedWorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope) when debugging
140+
- `globalThis` is one object (that I could find so far) which **is shared** between our extension and test scripts. A guess to why is that the main script spawns another web worker (for unit tests) and passes on the `DedicatedWorkerGlobalScope`. See [`"Workers can also spawn other workers"`](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers).
141+
- `globalThis` returns `global` in Node.js, or a `WorkerGlobalScope` in the browser

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4238,8 +4238,8 @@
42384238
"reset": "npm run clean -- node_modules && npm install",
42394239
"copyFiles": "ts-node ./scripts/build/copyFiles.ts",
42404240
"buildScripts": "npm run generateClients && npm run generatePackage && npm run generateNonCodeFiles && npm run copyFiles",
4241-
"buildBrowser": "npm run clean && npm run buildScripts && webpack --config webpack.browser.config.js --mode production && npm run copyFiles",
4242-
"runInBrowser": "npm run buildBrowser && npx @vscode/test-web --open-devtools --extensionDevelopmentPath=. .",
4241+
"buildBrowserDebug": "npm run clean && npm run buildScripts && webpack --config webpack.browser.config.js",
4242+
"runInBrowser": "npm run buildBrowserDebug && npx @vscode/test-web --open-devtools --extensionDevelopmentPath=. .",
42434243
"compile": "npm run clean && npm run buildScripts && webpack --mode development && npm run copyFiles",
42444244
"watch": "npm run clean && npm run buildScripts && tsc -watch -p ./",
42454245
"postinstall": "npm run generateTelemetry && npm run generateConfigurationAttributes && npm run buildCustomLintPlugin",

src/common/browserUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
*/
55

66
import * as vscode from 'vscode'
7+
import globals from '../shared/extensionGlobals'
78

8-
let inBrowser = false
99
/** Set the value of if we are in the browser. Impacts {@link isInBrowser}. */
1010
export function setInBrowser(value: boolean) {
11-
inBrowser = value
11+
globals.isInBrowser = value
1212
vscode.commands.executeCommand('setContext', 'aws.isWebExtHost', true).then(undefined, e => {
1313
console.error('setContext failed: %s', (e as Error).message)
1414
})
1515
}
1616
/** Return true if we are running in the browser, false otherwise. */
1717
export function isInBrowser() {
18-
return inBrowser
18+
return (globals.isInBrowser ??= false)
1919
}

src/shared/extensionGlobals.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,27 @@ function getBrowserAlternatives() {
7575
return alternatives
7676
}
7777

78-
const globals = { clock: copyClock() } as ToolkitGlobals
78+
/**
79+
* Why was the following implemented this way?
80+
*
81+
* With the introduction of browser support + setting up browser unit tests,
82+
* it was discovered that variables declared at the global scope were not available
83+
* between the actual extension code and the unit tests. Though for the regular desktop/node
84+
* tests, global scope variables WERE shared and the following was not required.
85+
*
86+
* So as a solution, any global scoped objects must be stored in `globalThis` so that if defined
87+
* in Browser-compatible extension code, it shares the same context/scope as the Browser unit test code.
88+
*
89+
* See `browser.md` for more info.
90+
*/
91+
function resolveGlobalsObject() {
92+
if ((globalThis as any).globals === undefined) {
93+
;(globalThis as any).globals = { clock: copyClock() } as ToolkitGlobals
94+
}
95+
return (globalThis as any).globals
96+
}
97+
98+
const globals: ToolkitGlobals = resolveGlobalsObject()
7999

80100
export function checkDidReload(context: ExtensionContext): boolean {
81101
return !!context.globalState.get<string>('ACTIVATION_LAUNCH_PATH_KEY')
@@ -145,4 +165,6 @@ interface ToolkitGlobals {
145165
endpoints: string
146166
lambdaSampleRequests: string
147167
}
168+
/** If this extension is running in the Browser (webworker), compared to running on the desktop (node) */
169+
isInBrowser: boolean
148170
}

src/testBrowser/temp.test.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/testBrowser/testRunner.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
/**
7+
* The following was influenced by this guide: https://code.visualstudio.com/api/extension-guides/web-extensions
8+
*/
9+
610
import 'mocha' // Imports mocha for the browser, defining the `mocha` global.
7-
import { activate as activateBrowserExtension } from '../extensionWeb'
8-
import vscode from 'vscode'
11+
import * as vscode from 'vscode'
12+
import { VSCODE_EXTENSION_ID } from '../shared/extensions'
913

10-
export function run(): Promise<void> {
14+
export async function run(): Promise<void> {
15+
await activateToolkitExtension()
1116
return new Promise(async (resolve, reject) => {
1217
setupMocha()
1318
gatherTestFiles()
14-
stubVscodeWindow()
15-
await runBrowserExtensionActivation()
1619

1720
try {
1821
runMochaTests(resolve, reject)
@@ -36,23 +39,14 @@ function gatherTestFiles() {
3639
importAll(require.context('.', true, /\.test$/))
3740
}
3841

39-
function stubVscodeWindow() {
40-
// We skip this for now since getTestWindow() has transitive imports
41-
// that import 'fs' and cause issues.
42-
// TODO: Go through the transitive imports and swap the 'fs' uses
43-
// with our own browser compatible ones.
44-
// globalSandbox.replace(vscode, 'window', getTestWindow())
45-
}
46-
4742
/**
48-
* This is the root function that activates the toolkit extension in a browser
49-
* context.
43+
* Typically extensions activate depending on their configuration in `package.json#activationEvents`, but in tests
44+
* there is a race condition for when the extension has finished activating and when we start the tests.
45+
*
46+
* So this function ensures the extension has fully activated.
5047
*/
51-
async function runBrowserExtensionActivation() {
52-
await activateBrowserExtension({
53-
logUri: vscode.Uri.parse('./tempLogFile.txt'),
54-
subscriptions: [],
55-
} as any)
48+
async function activateToolkitExtension() {
49+
await vscode.extensions.getExtension(VSCODE_EXTENSION_ID.awstoolkit)?.activate()
5650
}
5751

5852
function runMochaTests(resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: any) => void) {

src/testBrowser/utils.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isInBrowser } from '../common/browserUtils'
88

99
describe('isInBrowser', function () {
1010
it('returns true when in browser', function () {
11+
// Note that this only works since the state is indirectly stored in `globalThis`, see browser.md for more info
1112
assert.strictEqual(isInBrowser(), true)
1213
})
1314
})

webpack.base.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const baseConfig = {
3232
path: path.resolve(__dirname, 'dist'),
3333
filename: '[name].js',
3434
libraryTarget: 'commonjs2',
35-
devtoolModuleFilenameTemplate: '../[resource-path]',
3635
},
3736
devtool: 'source-map',
3837
externals: {

0 commit comments

Comments
 (0)