Files

758 lines
30 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { test, expect, stripAnsi } from './playwright-test-fixtures';
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
const POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'ok' : '✓ ';
const NEGATIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'x ' : '✘ ';
for (const useIntermediateMergeReport of [false, true] as const) {
test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
test.use({ useIntermediateMergeReport });
test('render each test with project name', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { projects: [
{ name: 'foo' },
{ name: 'bar' },
] };
`,
'a.test.ts': `
const { test, expect } = require('@playwright/test');
test('fails', async ({}) => {
expect(1).toBe(0);
});
test('passes', async ({}) => {
expect(0).toBe(0);
});
test.skip('skipped', async () => {
});
`,
}, { reporter: 'list', workers: '1' });
const text = result.output;
expect(text).toContain(`${NEGATIVE_STATUS_MARK} 1 [foo] › a.test.ts:3:11 › fails`);
expect(text).toContain(`${POSITIVE_STATUS_MARK} 2 [foo] › a.test.ts:6:11 › passes`);
expect(text).toContain(`- 3 [foo] › a.test.ts:9:16 › skipped`);
expect(text).toContain(`${NEGATIVE_STATUS_MARK} 4 [bar] › a.test.ts:3:11 › fails`);
expect(text).toContain(`${POSITIVE_STATUS_MARK} 5 [bar] › a.test.ts:6:11 › passes`);
expect(text).toContain(`- 6 [bar] › a.test.ts:9:16 › skipped`);
expect(result.exitCode).toBe(1);
});
test('render steps', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
await test.step('outer 1.0', async () => {
await test.step('inner 1.1', async () => {});
await test.step('inner 1.2', async () => {});
});
await test.step('outer 2.0', async () => {
await test.step('inner 2.1', async () => {});
await test.step('inner 2.2', async () => {});
});
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
const text = result.output;
const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
lines.pop(); // Remove last item that contains [v] and time in ms.
expect(lines).toEqual([
'#0 : 1 a.test.ts:3:15 › passes',
'#1 : 1.1 passes › outer 1.0',
'#2 : 1.2 passes › outer 1.0 › inner 1.1',
'#2 : 1.2 passes › outer 1.0 › inner 1.1 (Xms)',
'#3 : 1.3 passes › outer 1.0 › inner 1.2',
'#3 : 1.3 passes › outer 1.0 › inner 1.2 (Xms)',
'#1 : 1.1 passes › outer 1.0 (Xms)',
'#4 : 1.4 passes › outer 2.0',
'#5 : 1.5 passes › outer 2.0 › inner 2.1',
'#5 : 1.5 passes › outer 2.0 › inner 2.1 (Xms)',
'#6 : 1.6 passes › outer 2.0 › inner 2.2',
'#6 : 1.6 passes › outer 2.0 › inner 2.2 (Xms)',
'#4 : 1.4 passes › outer 2.0 (Xms)',
]);
});
test('render steps inline', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
await test.step('outer 1.0', async () => {
await test.step('inner 1.1', async () => {});
await test.step('inner 1.2', async () => {});
});
await test.step('outer 2.0', async () => {
await test.step('inner 2.1', async () => {});
await test.step('inner 2.2', async () => {});
});
});`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
const text = result.output;
const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
lines.pop(); // Remove last item that contains [v] and time in ms.
expect(lines).toEqual([
'#0 : 1 a.test.ts:3:11 › passes',
'#0 : 1 a.test.ts:3:11 › passes › outer 1.0',
'#0 : 1 a.test.ts:3:11 › passes › outer 1.0 › inner 1.1',
'#0 : 1 a.test.ts:3:11 › passes › outer 1.0',
'#0 : 1 a.test.ts:3:11 › passes › outer 1.0 › inner 1.2',
'#0 : 1 a.test.ts:3:11 › passes › outer 1.0',
'#0 : 1 a.test.ts:3:11 › passes',
'#0 : 1 a.test.ts:3:11 › passes › outer 2.0',
'#0 : 1 a.test.ts:3:11 › passes › outer 2.0 › inner 2.1',
'#0 : 1 a.test.ts:3:11 › passes › outer 2.0',
'#0 : 1 a.test.ts:3:11 › passes › outer 2.0 › inner 2.2',
'#0 : 1 a.test.ts:3:11 › passes › outer 2.0',
'#0 : 1 a.test.ts:3:11 › passes',
]);
});
test('render steps in non-TTY mode', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
await test.step('outer 1.0', async () => {
await test.step('inner 1.1', async () => {});
await test.step('inner 1.2', async () => {});
});
await test.step('outer 2.0', async () => {
await test.step('inner 2.1', async () => {});
await test.step('inner 2.2', async () => {});
});
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_LIST_PRINT_STEPS: '1' });
const text = result.output;
const lines = text.split('\n').filter(l => l.match(/^#.* :/)).map(l => l.replace(/[.\d]+m?s/, 'Xms'));
expect(lines).toEqual([
'#0 : 1.1 a.test.ts:3:15 › passes › outer 1.0 › inner 1.1 (Xms)',
'#1 : 1.2 a.test.ts:3:15 › passes › outer 1.0 › inner 1.2 (Xms)',
'#2 : 1.3 a.test.ts:3:15 › passes › outer 1.0 (Xms)',
'#3 : 1.4 a.test.ts:3:15 › passes › outer 2.0 › inner 2.1 (Xms)',
'#4 : 1.5 a.test.ts:3:15 › passes › outer 2.0 › inner 2.2 (Xms)',
'#5 : 1.6 a.test.ts:3:15 › passes › outer 2.0 (Xms)',
`#6 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › passes (Xms)`,
]);
});
test('very long console line should not mess terminal', async ({ runInlineTest }) => {
const TTY_WIDTH = 80;
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
console.log('a'.repeat(80) + 'b'.repeat(20));
});
`,
}, { reporter: 'list' }, { PLAYWRIGHT_FORCE_TTY: TTY_WIDTH + '' });
const renderedText = simpleAnsiRenderer(result.rawOutput, TTY_WIDTH);
if (process.platform === 'win32')
expect(renderedText).toContain(' ok 1 a.test.ts:3:15 › passes');
else
expect(renderedText).toContain(' ✓ 1 a.test.ts:3:15 › passes');
expect(renderedText).not.toContain(' 1 a.test.ts:3:15 › passes');
expect(renderedText).toContain('a'.repeat(80) + '\n' + 'b'.repeat(20));
});
test('render retries', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('flaky', async ({}, testInfo) => {
expect(testInfo.retry).toBe(1);
});
`,
}, { reporter: 'list', retries: '1' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
const text = result.output;
const lines = text.split('\n').filter(l => l.startsWith('#0 :') || l.startsWith('#1 :')).map(l => l.replace(/\d+(\.\d+)?m?s/, 'XXms'));
expect(lines).toEqual([
`#0 : 1 a.test.ts:3:15 › flaky`,
`#0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:15 › flaky (XXms)`,
`#1 : 2 a.test.ts:3:15 › flaky (retry #1)`,
`#1 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:3:15 › flaky (retry #1) (XXms)`,
]);
});
test('should truncate long test names', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { projects: [
{ name: 'foo' },
] };
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('failure in very long name', async ({}) => {
expect(1).toBe(0);
});
test('passes', async ({}) => {
});
test('passes 2 long name', async () => {
});
test.skip('skipped very long name', async () => {
});
`,
}, { reporter: 'list', retries: 0 }, { PLAYWRIGHT_FORCE_TTY: '50' });
expect(result.exitCode).toBe(1);
const lines = result.rawOutput.split('\n').map(line => line.split('\x1B[22m\x1B[1E')).flat().map(line => stripAnsi(line)).filter(line => line.trim()).slice(1, 9);
expect(lines.every(line => line.length <= 50)).toBe(true);
expect(lines[0]).toBe(` 1 …a.test.ts:3:15 › failure in very long name`);
expect(lines[1]).toContain(`${NEGATIVE_STATUS_MARK} 1 …`);
expect(lines[1]).toContain(`:3:15 › failure in very long name (`);
expect(lines[1].length).toBe(50);
expect(lines[2]).toBe(` 2 [foo] › a.test.ts:6:15 › passes`);
expect(lines[3]).toContain(`${POSITIVE_STATUS_MARK} 2 [foo] › a.test.ts:6:15 › passes (`);
expect(lines[4]).toBe(` 3 [foo] › a.test.ts:8:15 › passes 2 long name`);
expect(lines[5]).toContain(`${POSITIVE_STATUS_MARK} 3 …`);
expect(lines[5]).toContain(`test.ts:8:15 › passes 2 long name (`);
expect(lines[5].length).toBe(50);
expect(lines[6]).toBe(` 4 …› a.test.ts:10:16 › skipped very long name`);
expect(lines[7]).toBe(` - 4 …› a.test.ts:10:16 › skipped very long name`);
});
test('render failed test steps', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
await test.step('outer 1.0', async () => {
await test.step('inner 1.1', async () => {
expect(1).toBe(2);
});
});
});
`,
}, { reporter: 'list' });
const text = result.output;
expect(text).toContain('1) a.test.ts:3:15 › passes › outer 1.0 › inner 1.1 ──');
expect(result.exitCode).toBe(1);
});
test('print failures inline with option', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
reporter: [['list', { printFailuresInline: true }]],
workers: 1,
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('fails early', async ({}) => {
expect(1).toBe(2);
});
test('runs later', async ({}) => {
});
`,
});
const text = result.output;
const failureHeader = '1) a.test.ts:3:15 › fails early';
const laterTestStatus = `${POSITIVE_STATUS_MARK} 2 a.test.ts:6:15 › runs later`;
const failureIndex = text.indexOf(failureHeader);
const laterTestIndex = text.indexOf(laterTestStatus);
expect(failureIndex).toBeGreaterThan(-1);
expect(laterTestIndex).toBeGreaterThan(failureIndex);
expect(text.indexOf('Error: expect(received).toBe(expected)', failureIndex)).toBeGreaterThan(failureIndex);
expect(text.indexOf(failureHeader, failureIndex + 1)).toBe(-1);
expect(result.exitCode).toBe(1);
});
test('print failures inline for a non-retriable error', async ({ runInlineTest }) => {
// A missing snapshot fails the test but is not retried. The reporter must
// still print the failure inline, even though the retry budget is not exhausted.
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
reporter: [['list', { printFailuresInline: true }]],
retries: 1,
workers: 1,
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('missing snapshot', async ({}) => {
expect('actual').toMatchSnapshot('foo.txt');
});
`,
});
const text = result.output;
const failureHeader = '1) a.test.ts:3:15 › missing snapshot';
const failureIndex = text.indexOf(failureHeader);
expect(failureIndex, 'failure should be printed inline').not.toBe(-1);
expect(text.indexOf(`A snapshot doesn't exist`, failureIndex)).toBeGreaterThan(failureIndex);
// It must not be retried, so there is exactly one failure block.
expect(text.indexOf(failureHeader, failureIndex + 1)).toBe(-1);
expect(result.exitCode).toBe(1);
});
test('print failures inline with retries', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
reporter: [['list', { printFailuresInline: true }]],
retries: 2,
workers: 1,
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('fails', async ({}, testInfo) => {
throw new Error('attempt ' + testInfo.retry);
});
`,
});
const text = result.output;
const failureHeader = '1) a.test.ts:3:15 › fails';
// Each attempt is printed once, under the same failure index.
expect(text.split(failureHeader).length - 1).toBe(3);
expect(text).not.toContain('2) a.test.ts:3:15 › fails');
for (const attempt of ['attempt 0', 'attempt 1', 'attempt 2'])
expect(text.split('Error: ' + attempt).length - 1).toBe(1);
expect(text.split('Retry #1').length - 1).toBe(1);
expect(text.split('Retry #2').length - 1).toBe(1);
// Retry sections come after the corresponding retry attempt.
expect(text.indexOf('Retry #1')).toBeGreaterThan(text.indexOf('Error: attempt 0'));
expect(text.indexOf('Retry #2')).toBeGreaterThan(text.indexOf('Error: attempt 1'));
expect(result.exitCode).toBe(1);
});
test('print failures inline for a flaky test', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
reporter: [['list', { printFailuresInline: true }]],
retries: 1,
workers: 1,
};
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('flaky', async ({}, testInfo) => {
if (testInfo.retry === 0)
throw new Error('attempt ' + testInfo.retry);
});
`,
});
const text = result.output;
const failureHeader = '1) a.test.ts:3:15 › flaky';
expect(text.split(failureHeader).length - 1).toBe(1);
expect(text.split('Error: attempt 0').length - 1).toBe(1);
expect(text).not.toContain('Retry #1');
expect(text).toContain('1 flaky');
expect(result.exitCode).toBe(0);
});
test('print stdio', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
await new Promise(resolve => process.stdout.write('line1', () => resolve()));
await new Promise(resolve => process.stdout.write('line2\\n', () => resolve()));
await new Promise(resolve => process.stderr.write(Buffer.from(''), () => resolve()));
});
test('passes 2', async ({}) => {
await new Promise(resolve => process.stdout.write('partial', () => resolve()));
});
test('passes 3', async ({}) => {
await new Promise(resolve => process.stdout.write('full\\n', () => resolve()));
});
test('passes 4', async ({}) => {
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(4);
const expected = [
'#0 : 1 a.test.ts:3:15 › passes',
'line1line2',
`#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › passes`,
'',
'#3 : 2 a.test.ts:9:15 › passes 2',
`partial#3 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:9:15 › passes 2`,
'',
'#5 : 3 a.test.ts:13:15 › passes 3',
'full',
`#5 : ${POSITIVE_STATUS_MARK} 3 a.test.ts:13:15 › passes 3`,
'#7 : 4 a.test.ts:17:15 › passes 4',
`#7 : ${POSITIVE_STATUS_MARK} 4 a.test.ts:17:15 › passes 4`,
];
const lines = result.output.split('\n');
const firstIndex = lines.indexOf(expected[0]);
expect(firstIndex, 'first line should be there').not.toBe(-1);
for (let i = 0; i < expected.length; ++i)
expect(lines[firstIndex + i]).toContain(expected[i]);
});
test('should update test status row only when TTY has not scrolled', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('A', async ({}) => {
for (let i = 0; i < 20; ++i) {
console.log('line ' + i);
}
});
test('B', async ({}) => {
// Go past end of the screen
for (let i = 20; i < 60; ++i) {
console.log('line ' + i);
}
// Should create new line
await test.step('First step', async () => {
console.log('step 1');
});
for (let i = 60; i < 80; ++i) {
console.log('line ' + i);
}
// Should update the new (not original) line
await test.step('Second step', async () => {
console.log('step 2');
});
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
const expected = [
'#0 : 1 a.test.ts:3:15 › A',
];
for (let i = 0; i < 20; ++i)
expected.push(`line ${i}`);
// Update to initial test status row
expected.push(`#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › A`);
expected.push(`#21 : 2 a.test.ts:9:15 › B`);
for (let i = 20; i < 60; ++i)
expected.push(`line ${i}`);
expected.push(`#62 : 2 a.test.ts:9:15 › B › First step`);
expected.push(`step 1`);
expected.push(`#62 : 2 a.test.ts:9:15 › B`);
for (let i = 60; i < 80; ++i)
expected.push(`line ${i}`);
expected.push(`#62 : 2 a.test.ts:9:15 › B › Second step`);
expected.push(`step 2`);
expected.push(`#62 : 2 a.test.ts:9:15 › B`);
expected.push(`#62 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:9:15 › B`);
const lines = result.output.split('\n');
const firstIndex = lines.indexOf(expected[0]);
expect(firstIndex, 'first line should be there').not.toBe(-1);
for (let i = 0; i < expected.length; ++i)
expect(lines[firstIndex + i]).toContain(expected[i]);
});
test('should update test status row only within configured TTY height', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('A', async ({}) => {
// No scroll
for (let i = 0; i < 60; ++i) {
console.log('line ' + i);
}
// Update original line
await test.step('First step', async () => {
console.log('step 1');
});
for (let i = 60; i < 120; ++i) {
console.log('line ' + i);
}
// Should create new line
await test.step('Second step', async () => {
console.log('step 2');
});
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80x80' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const expected = [
'#0 : 1 a.test.ts:3:15 › A',
];
for (let i = 0; i < 60; ++i)
expected.push(`line ${i}`);
// Update to initial test status row
expected.push(`#0 : 1 a.test.ts:3:15 › A › First step`);
expected.push(`step 1`);
expected.push(`#0 : 1 a.test.ts:3:15 › A`);
for (let i = 60; i < 120; ++i)
expected.push(`line ${i}`);
expected.push(`#122 : 1 a.test.ts:3:15 › A › Second step`);
expected.push(`step 2`);
expected.push(`#122 : 1 a.test.ts:3:15 › A`);
expected.push(`#122 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › A`);
const lines = result.output.split('\n');
const firstIndex = lines.indexOf(expected[0]);
expect(firstIndex, 'first line should be there').not.toBe(-1);
for (let i = 0; i < expected.length; ++i)
expect(lines[firstIndex + i]).toContain(expected[i]);
});
});
}
test.describe('onTestPaused', () => {
test.skip(process.platform === 'win32', 'No SIGINT on windows');
test('pause at end', async ({ interactWithTestRunner }) => {
const runner = await interactWithTestRunner({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('foo', async ({}) => {
});
test.afterEach(() => {
console.log('Running teardown');
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PWPAUSE: '1' });
await runner.waitForOutput('Paused at test end. Press Ctrl+C to end.');
const { exitCode } = await runner.kill('SIGINT');
expect(exitCode).toBe(130);
const output = stripAnsi(runner.output).replace(/\d+(\.\d+)?m?s/g, 'Xms');
expect(output).toEqual(`
Running 1 test using 1 worker
#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:13 › foo (Xms)
#1 : Paused at test end. Press Ctrl+C to end.
Running teardown
1 interrupted
a.test.ts:3:13 › foo ───────────────────────────────────────────────────────────────────────────
`);
});
test('pause at end - error in teardown', async ({ interactWithTestRunner }) => {
const runner = await interactWithTestRunner({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('foo', async ({}) => {
});
test.afterEach(() => {
throw new Error('teardown error');
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PWPAUSE: '1' });
await runner.waitForOutput('Paused at test end. Press Ctrl+C to end.');
const { exitCode } = await runner.kill('SIGINT');
expect(exitCode).toBe(130);
const output = stripAnsi(runner.output).replace(/\d+(\.\d+)?m?s/g, 'Xms');
expect(output).toEqual(`
Running 1 test using 1 worker
#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:13 › foo (Xms)
#1 : Paused at test end. Press Ctrl+C to end.
1) a.test.ts:3:13 › foo ──────────────────────────────────────────────────────────────────────────
Test was interrupted.
Error: teardown error
5 |
6 | test.afterEach(() => {
> 7 | throw new Error('teardown error');
| ^
8 | });
9 |
at ${test.info().outputPath('a.test.ts')}:7:17
Error Context: test-results/a-foo/error-context.md
1 interrupted
a.test.ts:3:13 › foo ───────────────────────────────────────────────────────────────────────────
`);
});
test('pause on error', async ({ interactWithTestRunner }) => {
const runner = await interactWithTestRunner({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('fails', async ({}) => {
expect.soft(2).toBe(3);
expect(3).toBe(4);
});
`,
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PWPAUSE: '1' });
await runner.waitForOutput('Paused on error. Press Ctrl+C to end.');
const { exitCode } = await runner.kill('SIGINT');
expect(exitCode).toBe(130);
const output = stripAnsi(runner.output).replace(/\d+(\.\d+)?m?s/g, 'Xms');
expect(output).toEqual(`
Running 1 test using 1 worker
#0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:13 › fails (Xms)
Error: expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: 2
2 | import { test, expect } from '@playwright/test';
3 | test('fails', async ({}) => {
> 4 | expect.soft(2).toBe(3);
| ^
5 | expect(3).toBe(4);
6 | });
7 |
at ${test.info().outputPath('a.test.ts')}:4:26
Error: expect(received).toBe(expected) // Object.is equality
Expected: 4
Received: 3
3 | test('fails', async ({}) => {
4 | expect.soft(2).toBe(3);
> 5 | expect(3).toBe(4);
| ^
6 | });
7 |
at ${test.info().outputPath('a.test.ts')}:5:21
#1 : Paused on error. Press Ctrl+C to end.
1 failed
a.test.ts:3:13 › fails ─────────────────────────────────────────────────────────────────────────
`);
});
});
test('strips ansi from test stdout when FORCE_COLOR=0', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
console.log('\\u001b[31mRED\\u001b[39mWHITE');
process.stderr.write('GREEN\\u001b[32mDOT\\u001b[39mEND\\n');
});
`,
}, { reporter: 'list' }, { FORCE_COLOR: '0', PLAYWRIGHT_FORCE_TTY: '80' });
expect(result.exitCode).toBe(0);
expect(result.rawOutput).toContain('REDWHITE');
expect(result.rawOutput).toContain('GREENDOTEND');
});
test('strips ansi from test stdout when NO_COLOR is set', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', async ({}) => {
console.log('\\u001b[31mRED\\u001b[39mWHITE');
process.stderr.write('GREEN\\u001b[32mDOT\\u001b[39mEND\\n');
});
`,
}, { reporter: 'list' }, { NO_COLOR: '1', FORCE_COLOR: '', PLAYWRIGHT_FORCE_TTY: '80' });
expect(result.exitCode).toBe(0);
expect(result.rawOutput).toContain('REDWHITE');
expect(result.rawOutput).toContain('GREENDOTEND');
});
function simpleAnsiRenderer(text, ttyWidth) {
let lineNumber = 0;
let columnNumber = 0;
const screenLines: string[][] = [];
const ensureScreenSize = () => {
if (lineNumber < 0)
throw new Error('Bad terminal navigation!');
while (lineNumber >= screenLines.length)
screenLines.push(new Array(ttyWidth).fill(''));
};
const print = ch => {
ensureScreenSize();
if (ch === '\n') {
columnNumber = 0;
++lineNumber;
} else {
screenLines[lineNumber][columnNumber++] = ch;
if (columnNumber === ttyWidth) {
columnNumber = 0;
++lineNumber;
}
}
ensureScreenSize();
};
let index = 0;
const ansiCodes = [...text.matchAll(/\u001B\[(\d*)(.)/g)];
for (const ansiCode of ansiCodes) {
const [matchText, codeValue, codeType] = ansiCode;
const code = (codeValue + codeType).toUpperCase();
while (index < ansiCode.index)
print(text[index++]);
if (codeType.toUpperCase() === 'E') {
// Go X lines down
lineNumber += +codeValue;
ensureScreenSize();
} else if (codeType.toUpperCase() === 'A') {
// Go X lines up
lineNumber -= +codeValue;
ensureScreenSize();
} else if (code === '2K') {
// Erase full line
ensureScreenSize();
screenLines[lineNumber] = new Array(ttyWidth).fill('');
} else if (code === '0G') {
// Go to start
columnNumber = 0;
} else {
// Unsupported ANSI code (e.g. all colors).
}
index += matchText.length;
}
while (index < text.length)
print(text[index++]);
return screenLines.map(line => line.join('')).join('\n');
}