Browse Source

test: bring new folio and migrate small amount of tests to it (#5994)

pull/6042/head
Dmitry Gozman 1 week ago
committed by GitHub
parent
commit
be79b3883b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 6903 additions and 226 deletions
  1. +1
    -1
      .eslintignore
  2. +50
    -3
      .github/workflows/tests.yml
  3. +3
    -1
      package.json
  4. +0
    -59
      test/android/browser.spec.ts
  5. +0
    -63
      test/android/device.spec.ts
  6. +0
    -61
      test/android/webview.spec.ts
  7. +0
    -12
      test/fixtures.spec.ts
  8. +57
    -0
      tests/android/browser.spec.ts
  9. +59
    -0
      tests/android/device.spec.ts
  10. +58
    -0
      tests/android/webview.spec.ts
  11. +0
    -0
      tests/assets/callback.js
  12. +4
    -4
      tests/browser.spec.ts
  13. +10
    -17
      tests/browsertype-basic.spec.ts
  14. +38
    -0
      tests/config/android.config.ts
  15. +81
    -0
      tests/config/androidEnv.ts
  16. +27
    -0
      tests/config/androidTest.ts
  17. +261
    -0
      tests/config/browserEnv.ts
  18. +28
    -0
      tests/config/browserTest.ts
  19. +62
    -0
      tests/config/default.config.ts
  20. +42
    -0
      tests/config/pageTest.ts
  21. +33
    -0
      tests/config/playwrightTest.ts
  22. +77
    -0
      tests/config/serverEnv.ts
  23. +24
    -0
      tests/config/serverTest.ts
  24. +1
    -0
      tests/folio/.gitignore
  25. +19
    -0
      tests/folio/cli.js
  26. +278
    -0
      tests/folio/src/cli.ts
  27. +387
    -0
      tests/folio/src/dispatcher.ts
  28. +53
    -0
      tests/folio/src/expect.ts
  29. +150
    -0
      tests/folio/src/expectType.ts
  30. +25
    -0
      tests/folio/src/globals.ts
  31. +171
    -0
      tests/folio/src/golden.ts
  32. +27
    -0
      tests/folio/src/index.ts
  33. +66
    -0
      tests/folio/src/ipc.ts
  34. +94
    -0
      tests/folio/src/loader.ts
  35. +246
    -0
      tests/folio/src/reporters/base.ts
  36. +57
    -0
      tests/folio/src/reporters/dot.ts
  37. +30
    -0
      tests/folio/src/reporters/empty.ts
  38. +136
    -0
      tests/folio/src/reporters/json.ts
  39. +185
    -0
      tests/folio/src/reporters/junit.ts
  40. +81
    -0
      tests/folio/src/reporters/line.ts
  41. +79
    -0
      tests/folio/src/reporters/list.ts
  42. +65
    -0
      tests/folio/src/reporters/multiplexer.ts
  43. +132
    -0
      tests/folio/src/runner.ts
  44. +218
    -0
      tests/folio/src/spec.ts
  45. +237
    -0
      tests/folio/src/test.ts
  46. +80
    -0
      tests/folio/src/transform.ts
  47. +194
    -0
      tests/folio/src/types.ts
  48. +139
    -0
      tests/folio/src/util.ts
  49. +119
    -0
      tests/folio/src/worker.ts
  50. +440
    -0
      tests/folio/src/workerRunner.ts
  51. +2222
    -0
      tests/folio/third_party/diff_match_patch.js
  52. +22
    -0
      tests/folio/tsconfig.json
  53. +5
    -5
      tests/jshandle-as-element.spec.ts
  54. +30
    -0
      tests/stack-trace.spec.ts

+ 1
- 1
.eslintignore View File

@ -13,5 +13,5 @@ utils/generate_types/test/test.ts
node_modules/
browser_patches/*/checkout/
browser_patches/chromium/output/
packages/**/*.d.ts
**/*.d.ts
output/

+ 50
- 3
.github/workflows/tests.yml View File

@ -38,10 +38,17 @@ jobs:
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
# Wrap `npm run` in a subshell to redirect STDERR to file.
# Enable core dumps in the subshell.
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json && node test/checkCoverage.js"
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npm run folio -- ${{ matrix.browser }} --reporter=dot,json"
env:
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json"
env:
BROWSER: ${{ matrix.browser }}
FOLIO_JSON_OUTPUT_NAME: "test-results/report.json"
# Checking coverage across two test suites is hard. Temporary disabled.
# - run: node test/checkCoverage.js
# env:
# BROWSER: ${{ matrix.browser }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always() && github.repository == 'microsoft/playwright' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-'))
- uses: actions/upload-artifact@v1
@ -66,6 +73,9 @@ jobs:
- run: npm ci
- run: npm run build
- run: node lib/cli/cli install-deps ${{ matrix.browser }} chromium
- run: npm run folio -- ${{ matrix.browser }} --reporter=dot,json
env:
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json
env:
BROWSER: ${{ matrix.browser }}
@ -96,6 +106,10 @@ jobs:
- run: npm ci
- run: npm run build
- run: node lib/cli/cli install-deps
- run: npm run folio -- ${{ matrix.browser }} --reporter=dot,json
shell: bash
env:
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json
shell: bash
env:
@ -150,6 +164,11 @@ jobs:
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
# Wrap `npm run` in a subshell to redirect STDERR to file.
# Enable core dumps in the subshell.
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npm run folio -- ${{ matrix.browser }} --reporter=dot,json"
if: ${{ always() }}
env:
HEADFUL: 1
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json"
if: ${{ always() }}
env:
@ -185,6 +204,10 @@ jobs:
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
# Wrap `npm run` in a subshell to redirect STDERR to file.
# Enable core dumps in the subshell.
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npm run folio -- chromium --reporter=dot,json"
env:
PWMODE: "${{ matrix.mode }}"
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json"
env:
BROWSER: "chromium"
@ -219,6 +242,10 @@ jobs:
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
# Wrap `npm run` in a subshell to redirect STDERR to file.
# Enable core dumps in the subshell.
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npm run folio -- ${{ matrix.browser }} --reporter=dot,json"
env:
PWVIDEO: 1
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --timeout=60000 --global-timeout=5400000 --retries=3 --reporter=dot,json -p video"
env:
BROWSER: ${{ matrix.browser }}
@ -249,8 +276,10 @@ jobs:
run: utils/avd_recreate.sh
- name: Start Android Emulator
run: utils/avd_start.sh
- name: Run device tests
run: npx folio test/android -p browserName=chromium --workers=1 --forbid-only --timeout=120000 --global-timeout=5400000 --retries=3 --reporter=dot,json
- name: Run tests
run: npm run build-folio && node tests/folio/cli.js --config=tests/config/android.config.ts
env:
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- name: Run page tests
run: npx folio test/page -p browserName=chromium --workers=1 --forbid-only --timeout=120000 --global-timeout=5400000 --retries=3 --reporter=dot,json
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
@ -286,6 +315,10 @@ jobs:
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
# Wrap `npm run` in a subshell to redirect STDERR to file.
# Enable core dumps in the subshell.
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npm run folio -- chromium --reporter=dot,json"
env:
PW_CHROMIUM_CHANNEL: "chrome"
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --timeout=60000 --global-timeout=5400000 --retries=3 --reporter=dot,json"
env:
BROWSER: "chromium"
@ -314,6 +347,11 @@ jobs:
- run: npm run build
# This only created problems, should we move ffmpeg back into npm?
- run: node lib/cli/cli install ffmpeg
- run: npm run folio -- chromium --reporter=dot,json
shell: bash
env:
PW_CHROMIUM_CHANNEL: "chrome"
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json
shell: bash
env:
@ -340,6 +378,10 @@ jobs:
- run: npm run build
# This only created problems, should we move ffmpeg back into npm?
- run: node lib/cli/cli install ffmpeg
- run: npm run folio -- chromium --reporter=dot,json
env:
PW_CHROMIUM_CHANNEL: "chrome"
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json
env:
BROWSER: "chromium"
@ -368,6 +410,11 @@ jobs:
- run: npm run build
# This only created problems, should we move ffmpeg back into npm?
- run: node lib/cli/cli install ffmpeg
- run: npm run folio -- chromium --reporter=dot,json
shell: bash
env:
PW_CHROMIUM_CHANNEL: "msedge"
FOLIO_JSON_OUTPUT_NAME: "test-results/report-new.json"
- run: npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json
shell: bash
env:


+ 3
- 1
package.json View File

@ -28,7 +28,9 @@
"check-deps": "node utils/check_deps.js",
"build-android-driver": "./utils/build_android_driver.sh",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
"build-storybook": "build-storybook -s public",
"build-folio": "tsc -p ./tests/folio",
"folio": "npm run build-folio && node tests/folio/cli.js --config=tests/config/default.config.ts"
},
"author": {
"name": "Microsoft Corporation"


+ 0
- 59
test/android/browser.spec.ts View File

@ -1,59 +0,0 @@
/**
* Copyright 2020 Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { folio } from '../fixtures';
const { it, expect } = folio;
if (process.env.PW_ANDROID_TESTS) {
it('androidDevice.model', async function({ androidDevice }) {
expect(androidDevice.model()).toBe('sdk_gphone_x86_arm');
});
it('androidDevice.launchBrowser', async function({ androidDevice }) {
const context = await androidDevice.launchBrowser();
const [page] = context.pages();
await page.goto('data:text/html,<title>Hello world!</title>');
expect(await page.title()).toBe('Hello world!');
await context.close();
});
it('should create new page', async function({ androidDevice }) {
const context = await androidDevice.launchBrowser();
const page = await context.newPage();
await page.goto('data:text/html,<title>Hello world!</title>');
expect(await page.title()).toBe('Hello world!');
await page.close();
await context.close();
});
it('should check', async function({ androidDevice }) {
const context = await androidDevice.launchBrowser();
const [page] = context.pages();
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
await page.check('input');
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
await page.close();
await context.close();
});
it('should be able to send CDP messages', async ({ androidDevice }) => {
const context = await androidDevice.launchBrowser();
const [page] = context.pages();
const client = await context.newCDPSession(page);
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
expect(evalResponse.result.value).toBe(3);
});
}

+ 0
- 63
test/android/device.spec.ts View File

@ -1,63 +0,0 @@
/**
* Copyright 2020 Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { PNG } from 'pngjs';
import { folio } from '../fixtures';
const { it, expect } = folio;
if (process.env.PW_ANDROID_TESTS) {
it('androidDevice.shell', async function({ androidDevice }) {
const output = await androidDevice.shell('echo 123');
expect(output.toString()).toBe('123\n');
});
it('androidDevice.open', async function({ androidDevice }) {
const socket = await androidDevice.open('shell:/bin/cat');
await socket.write(Buffer.from('321\n'));
const output = await new Promise(resolve => socket.on('data', resolve));
expect(output.toString()).toBe('321\n');
const closedPromise = new Promise(resolve => socket.on('close', resolve));
await socket.close();
await closedPromise;
});
it('androidDevice.screenshot', async function({ androidDevice, testInfo }) {
const path = testInfo.outputPath('screenshot.png');
const result = await androidDevice.screenshot({ path });
const buffer = fs.readFileSync(path);
expect(result.length).toBe(buffer.length);
const { width, height} = PNG.sync.read(result);
expect(width).toBe(1080);
expect(height).toBe(1920);
});
it('androidDevice.push', async function({ androidDevice }) {
await androidDevice.shell('rm /data/local/tmp/hello-world');
await androidDevice.push(Buffer.from('hello world'), '/data/local/tmp/hello-world');
const data = await androidDevice.shell('cat /data/local/tmp/hello-world');
expect(data).toEqual(Buffer.from('hello world'));
});
it('androidDevice.fill', test => {
test.fixme(!!process.env.CI, 'Hangs on the bots');
}, async function({ androidDevice }) {
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
await androidDevice.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'Hello');
expect((await androidDevice.info({ res: 'org.chromium.webview_shell:id/url_field' })).text).toBe('Hello');
});
}

+ 0
- 61
test/android/webview.spec.ts View File

@ -1,61 +0,0 @@
/**
* Copyright 2020 Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { folio } from '../fixtures';
const { it, expect } = folio;
if (process.env.PW_ANDROID_TESTS) {
it('androidDevice.webView', async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
expect(webview.pkg()).toBe('org.chromium.webview_shell');
expect(androidDevice.webViews().length).toBe(1);
});
it('webView.page', async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
const page = await webview.page();
expect(page.url()).toBe('about:blank');
});
it('should navigate page internally', async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
const page = await webview.page();
await page.goto('data:text/html,<title>Hello world!</title>');
expect(await page.title()).toBe('Hello world!');
});
it('should navigate page externally', test => {
test.fixme(!!process.env.CI, 'Hangs on the bots');
}, async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
const page = await webview.page();
await androidDevice.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'data:text/html,<title>Hello world!</title>');
await Promise.all([
page.waitForNavigation(),
androidDevice.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter')
]);
expect(await page.title()).toBe('Hello world!');
});
}

+ 0
- 12
test/fixtures.spec.ts View File

@ -17,9 +17,6 @@
import { folio } from './remoteServer.fixture';
import { execSync } from 'child_process';
import path from 'path';
import * as stackTrace from '../src/utils/stackTrace';
import { setUnderTest } from '../src/utils/utils';
import type { Browser } from '../index';
const { it, describe, expect, beforeEach, afterEach } = folio;
@ -146,12 +143,3 @@ describe('stalling signals', (suite, { platform, headful }) => {
expect(await stallingRemoteServer.childExitCode()).toBe(130);
});
});
it('caller file path', async ({}) => {
setUnderTest();
const callme = require('./fixtures/callback');
const filePath = callme(() => {
return stackTrace.getCallerFilePath(path.join(__dirname, 'fixtures') + path.sep);
});
expect(filePath).toBe(__filename);
});

+ 57
- 0
tests/android/browser.spec.ts View File

@ -0,0 +1,57 @@
/**
* Copyright 2020 Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from '../config/androidTest';
test('androidDevice.model', async function({ androidDevice }) {
expect(androidDevice.model()).toBe('sdk_gphone_x86_arm');
});
test('androidDevice.launchBrowser', async function({ androidDevice }) {
const context = await androidDevice.launchBrowser();
const [page] = context.pages();
await page.goto('data:text/html,<title>Hello world!</title>');
expect(await page.title()).toBe('Hello world!');
await context.close();
});
test('should create new page', async function({ androidDevice }) {
const context = await androidDevice.launchBrowser();
const page = await context.newPage();
await page.goto('data:text/html,<title>Hello world!</title>');
expect(await page.title()).toBe('Hello world!');
await page.close();
await context.close();
});
test('should check', async function({ androidDevice }) {
const context = await androidDevice.launchBrowser();
const [page] = context.pages();
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
await page.check('input');
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
await page.close();
await context.close();
});
test('should be able to send CDP messages', async ({ androidDevice }) => {
const context = await androidDevice.launchBrowser();
const [page] = context.pages();
const client = await context.newCDPSession(page);
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
expect(evalResponse.result.value).toBe(3);
});

+ 59
- 0
tests/android/device.spec.ts View File

@ -0,0 +1,59 @@
/**
* Copyright 2020 Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { PNG } from 'pngjs';
import { test, expect } from '../config/androidTest';
test('androidDevice.shell', async function({ androidDevice }) {
const output = await androidDevice.shell('echo 123');
expect(output.toString()).toBe('123\n');
});
test('androidDevice.open', async function({ androidDevice }) {
const socket = await androidDevice.open('shell:/bin/cat');
await socket.write(Buffer.from('321\n'));
const output = await new Promise(resolve => socket.on('data', resolve));
expect(output.toString()).toBe('321\n');
const closedPromise = new Promise(resolve => socket.on('close', resolve));
await socket.close();
await closedPromise;
});
test('androidDevice.screenshot', async function({ androidDevice }, testInfo) {
const path = testInfo.outputPath('screenshot.png');
const result = await androidDevice.screenshot({ path });
const buffer = fs.readFileSync(path);
expect(result.length).toBe(buffer.length);
const { width, height} = PNG.sync.read(result);
expect(width).toBe(1080);
expect(height).toBe(1920);
});
test('androidDevice.push', async function({ androidDevice }) {
await androidDevice.shell('rm /data/local/tmp/hello-world');
await androidDevice.push(Buffer.from('hello world'), '/data/local/tmp/hello-world');
const data = await androidDevice.shell('cat /data/local/tmp/hello-world');
expect(data).toEqual(Buffer.from('hello world'));
});
test('androidDevice.fill', async function({ androidDevice }) {
test.fixme(!!process.env.CI, 'Hangs on the bots');
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
await androidDevice.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'Hello');
expect((await androidDevice.info({ res: 'org.chromium.webview_shell:id/url_field' })).text).toBe('Hello');
});

+ 58
- 0
tests/android/webview.spec.ts View File

@ -0,0 +1,58 @@
/**
* Copyright 2020 Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from '../config/androidTest';
test('androidDevice.webView', async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
expect(webview.pkg()).toBe('org.chromium.webview_shell');
expect(androidDevice.webViews().length).toBe(1);
});
test('webView.page', async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
const page = await webview.page();
expect(page.url()).toBe('about:blank');
});
test('should navigate page internally', async function({ androidDevice }) {
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
const page = await webview.page();
await page.goto('data:text/html,<title>Hello world!</title>');
expect(await page.title()).toBe('Hello world!');
});
test('should navigate page externally', async function({ androidDevice }) {
test.fixme(!!process.env.CI, 'Hangs on the bots');
expect(androidDevice.webViews().length).toBe(0);
await androidDevice.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
const webview = await androidDevice.webView({ pkg: 'org.chromium.webview_shell' });
const page = await webview.page();
await androidDevice.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'data:text/html,<title>Hello world!</title>');
await Promise.all([
page.waitForNavigation(),
androidDevice.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter')
]);
expect(await page.title()).toBe('Hello world!');
});

test/fixtures/callback.js → tests/assets/callback.js View File


test/browser.spec.ts → tests/browser.spec.ts View File


test/browsertype-basic.spec.ts → tests/browsertype-basic.spec.ts View File


+ 38
- 0
tests/config/android.config.ts View File

@ -0,0 +1,38 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { setConfig, Config } from '../folio/out';
import * as path from 'path';
import { test as pageTest } from './pageTest';
import { test as androidTest } from './androidTest';
import { ServerEnv } from './serverEnv';
import { AndroidEnv, AndroidPageEnv } from './androidEnv';
const config: Config = {
testDir: path.join(__dirname, '..'),
timeout: 120000,
globalTimeout: 5400000,
};
if (process.env.CI) {
config.workers = 1;
config.forbidOnly = true;
config.retries = 3;
}
setConfig(config);
const serverEnv = new ServerEnv();
pageTest.runWith('android', serverEnv, new AndroidPageEnv(), {});
androidTest.runWith('android', serverEnv, new AndroidEnv(), {});

+ 81
- 0
tests/config/androidEnv.ts View File

@ -0,0 +1,81 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Env, WorkerInfo, TestInfo } from '../folio/out';
import type { AndroidDevice, BrowserContext } from '../../index';
import * as os from 'os';
import { AndroidTestArgs } from './androidTest';
import { PageTestArgs } from './pageTest';
require('../../lib/utils/utils').setUnderTest();
const playwright: typeof import('../../index') = require('../../index');
export class AndroidEnv implements Env<AndroidTestArgs> {
protected _device?: AndroidDevice;
async beforeAll(workerInfo: WorkerInfo) {
this._device = (await playwright._android.devices())[0];
await this._device.shell('am force-stop org.chromium.webview_shell');
await this._device.shell('am force-stop com.android.chrome');
this._device.setDefaultTimeout(120000);
}
async beforeEach(testInfo: TestInfo) {
// Use chromium screenshots.
testInfo.snapshotPathSegment = 'chromium';
return {
mode: 'default' as const,
isChromium: true,
isFirefox: false,
isWebKit: false,
isWindows: os.platform() === 'win32',
isMac: os.platform() === 'darwin',
isLinux: os.platform() === 'linux',
platform: os.platform() as ('win32' | 'darwin' | 'linux'),
video: false,
toImpl: (playwright as any)._toImpl,
playwright,
androidDevice: this._device!,
};
}
async afterAll(workerInfo: WorkerInfo) {
if (this._device)
await this._device.close();
this._device = undefined;
}
}
export class AndroidPageEnv extends AndroidEnv implements Env<PageTestArgs> {
private _context?: BrowserContext;
async beforeAll(workerInfo: WorkerInfo) {
await super.beforeAll(workerInfo);
this._context = await this._device!.launchBrowser();
}
async beforeEach(testInfo: TestInfo) {
const result = await super.beforeEach(testInfo);
for (const page of this._context!.pages())
await page.close();
const page = await this._context!.newPage();
return {
...result,
androidDevice: undefined,
page,
};
}
}

+ 27
- 0
tests/config/androidTest.ts View File

@ -0,0 +1,27 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { newTestType } from '../folio/out';
import type { AndroidDevice } from '../../index';
import type { CommonTestArgs } from './pageTest';
import type { ServerTestArgs } from './serverTest';
export { expect } from 'folio';
export type AndroidTestArgs = CommonTestArgs & {
androidDevice: AndroidDevice;
};
export const test = newTestType<AndroidTestArgs & ServerTestArgs>();

+ 261
- 0
tests/config/browserEnv.ts View File

@ -0,0 +1,261 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Env, WorkerInfo, TestInfo } from '../folio/out';
import type { Browser, BrowserContext, BrowserContextOptions, BrowserType, LaunchOptions } from '../../index';
import { installCoverageHooks } from '../../test/coverage';
import { start } from '../../lib/outofprocess';
import { PlaywrightClient } from '../../lib/remote/playwrightClient';
import { removeFolders } from '../../lib/utils/utils';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as util from 'util';
import * as childProcess from 'child_process';
import { PlaywrightTestArgs } from './playwrightTest';
import { BrowserTestArgs } from './browserTest';
const mkdtempAsync = util.promisify(fs.mkdtemp);
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
type TestOptions = {
mode: 'default' | 'driver' | 'service';
video?: boolean;
trace?: boolean;
};
class DriverMode {
private _playwrightObject: any;
async setup(workerInfo: WorkerInfo) {
this._playwrightObject = await start();
return this._playwrightObject;
}
async teardown() {
await this._playwrightObject.stop();
}
}
class ServiceMode {
private _playwrightObejct: any;
private _client: any;
private _serviceProcess: childProcess.ChildProcess;
async setup(workerInfo: WorkerInfo) {
const port = 9407 + workerInfo.workerIndex * 2;
this._serviceProcess = childProcess.fork(path.join(__dirname, '..', '..', 'lib', 'service.js'), [String(port)], {
stdio: 'pipe'
});
this._serviceProcess.stderr.pipe(process.stderr);
await new Promise<void>(f => {
this._serviceProcess.stdout.on('data', data => {
if (data.toString().includes('Listening on'))
f();
});
});
this._serviceProcess.unref();
this._serviceProcess.on('exit', this._onExit);
this._client = await PlaywrightClient.connect(`ws://localhost:${port}/ws`);
this._playwrightObejct = this._client.playwright();
return this._playwrightObejct;
}
async teardown() {
await this._client.close();
this._serviceProcess.removeListener('exit', this._onExit);
const processExited = new Promise(f => this._serviceProcess.on('exit', f));
this._serviceProcess.kill();
await processExited;
}
private _onExit(exitCode, signal) {
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
}
}
class DefaultMode {
async setup(workerInfo: WorkerInfo) {
return require('../../index');
}
async teardown() {
}
}
export class PlaywrightEnv implements Env<PlaywrightTestArgs> {
private _mode: DriverMode | ServiceMode | DefaultMode;
private _browserName: BrowserName;
protected _options: LaunchOptions & TestOptions;
protected _browserOptions: LaunchOptions;
private _playwright: typeof import('../../index');
protected _browserType: BrowserType<Browser>;
private _coverage: ReturnType<typeof installCoverageHooks> | undefined;
private _userDataDirs: string[] = [];
private _persistentContext: BrowserContext | undefined;
constructor(browserName: BrowserName, options: LaunchOptions & TestOptions) {
this._browserName = browserName;
this._options = options;
this._mode = {
default: new DefaultMode(),
service: new ServiceMode(),
driver: new DriverMode(),
}[this._options.mode];
}
async beforeAll(workerInfo: WorkerInfo) {
this._coverage = installCoverageHooks(this._browserName);
require('../../lib/utils/utils').setUnderTest();
this._playwright = await this._mode.setup(workerInfo);
this._browserType = this._playwright[this._browserName];
this._browserOptions = {
...this._options,
handleSIGINT: false,
};
}
private async _createUserDataDir() {
// We do not put user data dir in testOutputPath,
// because we do not want to upload them as test result artifacts.
//
// Additionally, it is impossible to upload user data dir after test run:
// - Firefox removes lock file later, presumably from another watchdog process?
// - WebKit has circular symlinks that makes CI go crazy.
const dir = await mkdtempAsync(path.join(os.tmpdir(), 'playwright-test-'));
this._userDataDirs.push(dir);
return dir;
}
private async _launchPersistent(options?: Parameters<BrowserType<Browser>['launchPersistentContext']>[1]) {
if (this._persistentContext)
throw new Error('can only launch one persitent context');
const userDataDir = await this._createUserDataDir();
this._persistentContext = await this._browserType.launchPersistentContext(userDataDir, { ...this._browserOptions, ...options });
const page = this._persistentContext.pages()[0];
return { context: this._persistentContext, page };
}
async beforeEach(testInfo: TestInfo) {
// Different screenshots per browser.
testInfo.snapshotPathSegment = this._browserName;
return {
playwright: this._playwright,
browserName: this._browserName,
browserType: this._browserType,
browserChannel: this._options.channel,
browserOptions: this._browserOptions,
isChromium: this._browserName === 'chromium',
isFirefox: this._browserName === 'firefox',
isWebKit: this._browserName === 'webkit',
isWindows: os.platform() === 'win32',
isMac: os.platform() === 'darwin',
isLinux: os.platform() === 'linux',
headful: !this._browserOptions.headless,
video: !!this._options.video,
mode: this._options.mode,
platform: os.platform() as ('win32' | 'darwin' | 'linux'),
createUserDataDir: this._createUserDataDir.bind(this),
launchPersistent: this._launchPersistent.bind(this),
toImpl: (this._playwright as any)._toImpl,
};
}
async afterEach(testInfo: TestInfo) {
await removeFolders(this._userDataDirs);
this._userDataDirs = [];
if (this._persistentContext) {
await this._persistentContext.close();
this._persistentContext = undefined;
}
}
async afterAll(workerInfo: WorkerInfo) {
const { coverage, uninstall } = this._coverage!;
uninstall();
const coveragePath = path.join(__dirname, '..', '..', 'test', 'coverage-report', workerInfo.workerIndex + '.json');
const coverageJSON = [...coverage.keys()].filter(key => coverage.get(key));
await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true });
await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8');
}
}
export class BrowserEnv extends PlaywrightEnv implements Env<BrowserTestArgs> {
private _browser: Browser | undefined;
private _contextOptions: BrowserContextOptions;
private _contexts: BrowserContext[] = [];
constructor(browserName: BrowserName, options: LaunchOptions & BrowserContextOptions & TestOptions) {
super(browserName, options);
this._contextOptions = options;
}
async beforeAll(workerInfo: WorkerInfo) {
await super.beforeAll(workerInfo);
this._browser = await this._browserType.launch(this._browserOptions);
}
async beforeEach(testInfo: TestInfo) {
const result = await super.beforeEach(testInfo);
const contextOptions = {
recordVideo: this._options.video ? { dir: testInfo.outputPath('') } : undefined,
_traceDir: this._options.trace ? testInfo.outputPath('') : undefined,
...this._contextOptions,
} as BrowserContextOptions;
const contextFactory = async (options: BrowserContextOptions = {}) => {
const context = await this._browser.newContext({ ...contextOptions, ...options });
this._contexts.push(context);
return context;
};
return {
...result,
browser: this._browser,
contextOptions: this._contextOptions as BrowserContextOptions,
contextFactory,
};
}
async afterEach(testInfo: TestInfo) {
for (const context of this._contexts)
await context.close();
this._contexts = [];
await super.afterEach(testInfo);
}
async afterAll(workerInfo: WorkerInfo) {
if (this._browser)
await this._browser.close();
this._browser = undefined;
await super.afterAll(workerInfo);
}
}
export class PageEnv extends BrowserEnv {
async beforeEach(testInfo: TestInfo) {
const result = await super.beforeEach(testInfo);
const context = await result.contextFactory();
const page = await context.newPage();
return {
...result,
context,
page,
};
}
}

+ 28
- 0
tests/config/browserTest.ts View File

@ -0,0 +1,28 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { newTestType } from '../folio/out';
import type { Browser, BrowserContextOptions, BrowserContext } from '../../index';
import type { PlaywrightTestArgs } from './playwrightTest';
export { expect } from 'folio';
export type BrowserTestArgs = PlaywrightTestArgs & {
browser: Browser;
contextOptions: BrowserContextOptions;
contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
};
export const test = newTestType<BrowserTestArgs>();

+ 62
- 0
tests/config/default.config.ts View File

@ -0,0 +1,62 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { setConfig, Config } from '../folio/out';
import * as path from 'path';
import { test as playwrightTest } from './playwrightTest';
import { test as browserTest } from './browserTest';
import { test as pageTest } from './pageTest';
import { PlaywrightEnv, BrowserEnv, PageEnv, BrowserName } from './browserEnv';
import { ServerEnv } from './serverEnv';
const config: Config = {
testDir: path.join(__dirname, '..'),
timeout: process.env.PWVIDEO ? 60000 : 30000,
globalTimeout: 5400000,
};
if (process.env.CI) {
config.workers = 1;
config.forbidOnly = true;
config.retries = 3;
}
setConfig(config);
const getExecutablePath = (browserName: BrowserName) => {
if (browserName === 'chromium' && process.env.CRPATH)
return process.env.CRPATH;
if (browserName === 'firefox' && process.env.FFPATH)
return process.env.FFPATH;
if (browserName === 'webkit' && process.env.WKPATH)
return process.env.WKPATH;
};
const serverEnv = new ServerEnv();
const browsers = ['chromium', 'webkit', 'firefox'] as BrowserName[];
for (const browserName of browsers) {
const executablePath = getExecutablePath(browserName);
const options = {
mode: (process.env.PWMODE || 'default') as ('default' | 'driver' | 'service'),
executablePath,
trace: !!process.env.PWTRACE,
headless: !process.env.HEADFUL,
channel: process.env.PW_CHROMIUM_CHANNEL as any,
video: !!process.env.PWVIDEO,
};
playwrightTest.runWith(browserName, serverEnv, new PlaywrightEnv(browserName, options), {});
browserTest.runWith(browserName, serverEnv, new BrowserEnv(browserName, options), {});
pageTest.runWith(browserName, serverEnv, new PageEnv(browserName, options), {});
}

+ 42
- 0
tests/config/pageTest.ts View File

@ -0,0 +1,42 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { newTestType } from '../folio/out';
import type { Page } from '../../index';
import type { ServerTestArgs } from './serverTest';
export { expect } from 'folio';
export type CommonTestArgs = {
mode: 'default' | 'driver' | 'service';
platform: 'win32' | 'darwin' | 'linux';
video: boolean;
playwright: typeof import('../../index');
toImpl: (rpcObject: any) => any;
isChromium: boolean;
isFirefox: boolean;
isWebKit: boolean;
isWindows: boolean;
isMac: boolean;
isLinux: boolean;
};
export type PageTestArgs = CommonTestArgs & {
page: Page;
};
export const test = newTestType<PageTestArgs & ServerTestArgs>();

+ 33
- 0
tests/config/playwrightTest.ts View File

@ -0,0 +1,33 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { newTestType } from '../folio/out';
import type { Browser, BrowserType, LaunchOptions, BrowserContext, Page } from '../../index';
import { CommonTestArgs } from './pageTest';
import type { ServerTestArgs } from './serverTest';
export { expect } from 'folio';
export type PlaywrightTestArgs = CommonTestArgs & {
browserName: 'chromium' | 'firefox' | 'webkit';
browserType: BrowserType<Browser>;
browserChannel: string | undefined;
browserOptions: LaunchOptions;
headful: boolean;
createUserDataDir: () => Promise<string>;
launchPersistent: (options?: Parameters<BrowserType<Browser>['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>;
};
export const test = newTestType<PlaywrightTestArgs & ServerTestArgs>();

+ 77
- 0
tests/config/serverEnv.ts View File

@ -0,0 +1,77 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { WorkerInfo, TestInfo } from '../folio/out';
import { TestServer } from '../../utils/testserver';
import * as path from 'path';
import socks from 'socksv5';
export class ServerEnv {
private _server: TestServer;
private _httpsServer: TestServer;
private _socksServer: any;
async beforeAll(workerInfo: WorkerInfo) {
const assetsPath = path.join(__dirname, '..', 'assets');
const cachedPath = path.join(__dirname, '..', 'assets', 'cached');
const port = 8907 + workerInfo.workerIndex * 2;
this._server = await TestServer.create(assetsPath, port);
this._server.enableHTTPCache(cachedPath);
const httpsPort = port + 1;
this._httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort);
this._httpsServer.enableHTTPCache(cachedPath);
this._socksServer = socks.createServer((info, accept, deny) => {
let socket;
if ((socket = accept(true))) {
// Catch and ignore ECONNRESET errors.
socket.on('error', () => {});
const body = '<html><title>Served by the SOCKS proxy</title></html>';
socket.end([
'HTTP/1.1 200 OK',
'Connection: close',
'Content-Type: text/html',
'Content-Length: ' + Buffer.byteLength(body),
'',
body
].join('\r\n'));
}
});
const socksPort = 9107 + workerInfo.workerIndex * 2;
this._socksServer.listen(socksPort, 'localhost');
this._socksServer.useAuth(socks.auth.None());
}
async beforeEach(testInfo: TestInfo) {
this._server.reset();
this._httpsServer.reset();
return {
asset: (p: string) => path.join(__dirname, '..', 'assets', p),
server: this._server,
httpsServer: this._httpsServer,
};
}
async afterAll(workerInfo: WorkerInfo) {
await Promise.all([
this._server.stop(),
this._httpsServer.stop(),
this._socksServer.close(),
]);
}
}

+ 24
- 0
tests/config/serverTest.ts View File

@ -0,0 +1,24 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestServer } from '../../utils/testserver';
export type ServerTestArgs = {
asset: (path: string) => string;
socksPort: number,
server: TestServer;
httpsServer: TestServer;
};

+ 1
- 0
tests/folio/.gitignore View File

@ -0,0 +1 @@
out/

+ 19
- 0
tests/folio/cli.js View File

@ -0,0 +1,19 @@
#!/usr/bin/env node
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('./out/cli');

+ 278
- 0
tests/folio/src/cli.ts View File

@ -0,0 +1,278 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { default as ignore } from 'fstream-ignore';
import * as commander from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import EmptyReporter from './reporters/empty';
import DotReporter from './reporters/dot';
import JSONReporter from './reporters/json';
import JUnitReporter from './reporters/junit';
import LineReporter from './reporters/line';
import ListReporter from './reporters/list';
import { Multiplexer } from './reporters/multiplexer';
import { Runner } from './runner';
import { Config, FullConfig, Reporter } from './types';
import { Loader } from './loader';
import { createMatcher } from './util';
export const reporters: { [name: string]: new () => Reporter } = {
'dot': DotReporter,
'json': JSONReporter,
'junit': JUnitReporter,
'line': LineReporter,
'list': ListReporter,
'null': EmptyReporter,
};
const availableReporters = Object.keys(reporters).map(r => `"${r}"`).join();
const defaultConfig: FullConfig = {
forbidOnly: false,
globalTimeout: 0,
grep: /.*/,
maxFailures: 0,
outputDir: path.resolve(process.cwd(), 'test-results'),
quiet: false,
repeatEach: 1,
retries: 0,
shard: null,
snapshotDir: '__snapshots__',
testDir: path.resolve(process.cwd()),
testIgnore: 'node_modules/**',
testMatch: '**/?(*.)+(spec|test).[jt]s',
timeout: 10000,
updateSnapshots: false,
workers: Math.ceil(require('os').cpus().length / 2),
};
const loadProgram = new commander.Command();
loadProgram.helpOption(false);
addRunnerOptions(loadProgram);
loadProgram.action(async command => {
try {
await runTests(command);
} catch (e) {
console.log(e);
process.exit(1);
}
});
loadProgram.parse(process.argv);
async function runTests(command: any) {
if (command.help === undefined) {
console.log(loadProgram.helpInformation());
process.exit(0);
}
const reporterList: string[] = command.reporter.split(',');
const reporterObjects: Reporter[] = reporterList.map(c => {
if (reporters[c])
return new reporters[c]();
try {
const p = path.resolve(process.cwd(), c);
return new (require(p).default)();
} catch (e) {
console.error('Invalid reporter ' + c, e);
process.exit(1);
}
});
const loader = new Loader();
loader.addConfig(defaultConfig);
function loadConfig(configName: string) {
const configFile = path.resolve(process.cwd(), configName);
if (fs.existsSync(configFile)) {
loader.loadConfigFile(configFile);
return true;
}
return false;
}
if (command.config) {
if (!loadConfig(command.config))
throw new Error(`${command.config} does not exist`);
} else if (!loadConfig('folio.config.ts') && !loadConfig('folio.config.js')) {
throw new Error(`Configuration file not found. Either pass --config, or create folio.config.(js|ts) file`);
}
loader.addConfig(configFromCommand(command));
loader.addConfig({ testMatch: normalizeFilePatterns(loader.config().testMatch) });
loader.addConfig({ testIgnore: normalizeFilePatterns(loader.config().testIgnore) });
const testDir = loader.config().testDir;
if (!fs.existsSync(testDir))
throw new Error(`${testDir} does not exist`);
if (!fs.statSync(testDir).isDirectory())
throw new Error(`${testDir} is not a directory`);
const allAliases = new Set(loader.runLists().map(s => s.alias));
const runListFilter: string[] = [];
const testFileFilter: string[] = [];
for (const arg of command.args) {
if (allAliases.has(arg))
runListFilter.push(arg);
else
testFileFilter.push(arg);
}
const allFiles = await collectFiles(testDir);
const testFiles = filterFiles(testDir, allFiles, testFileFilter, createMatcher(loader.config().testMatch), createMatcher(loader.config().testIgnore));
for (const file of testFiles)
loader.loadTestFile(file);
const reporter = new Multiplexer(reporterObjects);
const runner = new Runner(loader, reporter, runListFilter.length ? runListFilter : undefined);
if (command.list) {
runner.list();
return;
}
const result = await runner.run();
if (result === 'sigint')
process.exit(130);
if (result === 'forbid-only') {
console.error('=====================================');
console.error(' --forbid-only found a focused test.');
console.error('=====================================');
process.exit(1);
}
if (result === 'no-tests') {
console.error('=================');
console.error(' no tests found.');
console.error('=================');
process.exit(1);
}
process.exit(result === 'failed' ? 1 : 0);
}
async function collectFiles(testDir: string): Promise<string[]> {
const entries: any[] = [];
let callback = () => {};
const promise = new Promise<void>(f => callback = f);
ignore({ path: testDir, ignoreFiles: ['.gitignore'] })
.on('child', (entry: any) => entries.push(entry))
.on('end', callback);
await promise;
return entries.filter(e => e.type === 'File').sort((a, b) => {
if (a.depth !== b.depth && (a.dirname.startsWith(b.dirname) || b.dirname.startsWith(a.dirname)))
return a.depth - b.depth;
return a.path > b.path ? 1 : (a.path < b.path ? -1 : 0);
}).map(e => e.path);
}
function filterFiles(base: string, files: string[], filters: string[], filesMatch: (value: string) => boolean, filesIgnore: (value: string) => boolean): string[] {
return files.filter(file => {
file = path.relative(base, file);
if (filesIgnore(file))
return false;
if (!filesMatch(file))
return false;
if (filters.length && !filters.find(filter => file.includes(filter)))