test: move test-runner-watch-mode helper into common · nodejs/node@8c31cbb

1-

import * as common from '../common/index.mjs';

1+

import '../common/index.mjs';

2+

import { skipIfNoWatch, refreshForTestRunnerWatch, testRunnerWatch } from '../common/watch.js';

23

import { describe, it, beforeEach } from 'node:test';

3-

import { once } from 'node:events';

4-

import assert from 'node:assert';

5-

import { spawn } from 'node:child_process';

6-

import { writeFileSync, renameSync, unlinkSync } from 'node:fs';

7-

import { setTimeout } from 'node:timers/promises';

8-

import tmpdir from '../common/tmpdir.js';

9410-

if (common.isIBMi)

11-

common.skip('IBMi does not support `fs.watch()`');

12-13-

if (common.isAIX)

14-

common.skip('folder watch capability is limited in AIX.');

15-16-

let fixturePaths;

17-18-

// This test updates these files repeatedly,

19-

// Reading them from disk is unreliable due to race conditions.

20-

const fixtureContent = {

21-

'dependency.js': 'module.exports = {};',

22-

'dependency.mjs': 'export const a = 1;',

23-

'test.js': `

24-

const test = require('node:test');

25-

require('./dependency.js');

26-

import('./dependency.mjs');

27-

import('data:text/javascript,');

28-

test('test has ran');`,

29-

};

30-31-

function refresh() {

32-

tmpdir.refresh();

33-

fixturePaths = Object.keys(fixtureContent)

34-

.reduce((acc, file) => ({ ...acc, [file]: tmpdir.resolve(file) }), {});

35-

Object.entries(fixtureContent)

36-

.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));

37-

}

38-39-

async function testWatch({

40-

fileToUpdate,

41-

file,

42-

action = 'update',

43-

fileToCreate,

44-

isolation,

45-

}) {

46-

const ran1 = Promise.withResolvers();

47-

const ran2 = Promise.withResolvers();

48-

const child = spawn(process.execPath,

49-

['--watch', '--test', '--test-reporter=spec',

50-

isolation ? `--test-isolation=${isolation}` : '',

51-

file ? fixturePaths[file] : undefined].filter(Boolean),

52-

{ encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path });

53-

let stdout = '';

54-

let currentRun = '';

55-

const runs = [];

56-57-

child.stdout.on('data', (data) => {

58-

stdout += data.toString();

59-

currentRun += data.toString();

60-

const testRuns = stdout.match(/duration_ms\s\d+/g);

61-

if (testRuns?.length >= 1) ran1.resolve();

62-

if (testRuns?.length >= 2) ran2.resolve();

63-

});

64-65-

const testUpdate = async () => {

66-

await ran1.promise;

67-

runs.push(currentRun);

68-

currentRun = '';

69-

const content = fixtureContent[fileToUpdate];

70-

const path = fixturePaths[fileToUpdate];

71-

writeFileSync(path, content);

72-

await setTimeout(common.platformTimeout(1000));

73-

await ran2.promise;

74-

runs.push(currentRun);

75-

child.kill();

76-

await once(child, 'exit');

77-78-

assert.strictEqual(runs.length, 2);

79-80-

for (const run of runs) {

81-

assert.match(run, /tests 1/);

82-

assert.match(run, /pass 1/);

83-

assert.match(run, /fail 0/);

84-

assert.match(run, /cancelled 0/);

85-

}

86-

};

87-88-

const testRename = async () => {

89-

await ran1.promise;

90-

runs.push(currentRun);

91-

currentRun = '';

92-

const fileToRenamePath = tmpdir.resolve(fileToUpdate);

93-

const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);

94-

renameSync(fileToRenamePath, newFileNamePath);

95-

await setTimeout(common.platformTimeout(1000));

96-

await ran2.promise;

97-

runs.push(currentRun);

98-

child.kill();

99-

await once(child, 'exit');

100-101-

assert.strictEqual(runs.length, 2);

102-103-

for (const run of runs) {

104-

assert.match(run, /tests 1/);

105-

assert.match(run, /pass 1/);

106-

assert.match(run, /fail 0/);

107-

assert.match(run, /cancelled 0/);

108-

}

109-

};

110-111-

const testDelete = async () => {

112-

await ran1.promise;

113-

runs.push(currentRun);

114-

currentRun = '';

115-

const fileToDeletePath = tmpdir.resolve(fileToUpdate);

116-

unlinkSync(fileToDeletePath);

117-

await setTimeout(common.platformTimeout(2000));

118-

ran2.resolve();

119-

runs.push(currentRun);

120-

child.kill();

121-

await once(child, 'exit');

122-123-

assert.strictEqual(runs.length, 2);

124-125-

for (const run of runs) {

126-

assert.doesNotMatch(run, /MODULE_NOT_FOUND/);

127-

}

128-

};

129-130-

const testCreate = async () => {

131-

await ran1.promise;

132-

runs.push(currentRun);

133-

currentRun = '';

134-

const newFilePath = tmpdir.resolve(fileToCreate);

135-

writeFileSync(newFilePath, 'module.exports = {};');

136-

await setTimeout(common.platformTimeout(1000));

137-

await ran2.promise;

138-

runs.push(currentRun);

139-

child.kill();

140-

await once(child, 'exit');

141-142-

for (const run of runs) {

143-

assert.match(run, /tests 1/);

144-

assert.match(run, /pass 1/);

145-

assert.match(run, /fail 0/);

146-

assert.match(run, /cancelled 0/);

147-

}

148-

};

149-150-

action === 'update' && await testUpdate();

151-

action === 'rename' && await testRename();

152-

action === 'delete' && await testDelete();

153-

action === 'create' && await testCreate();

154-

}

5+

skipIfNoWatch();

15561567

describe('test runner watch mode', () => {

157-

beforeEach(refresh);

8+

beforeEach(refreshForTestRunnerWatch);

1589

for (const isolation of ['none', 'process']) {

15910

describe(`isolation: ${isolation}`, () => {

16011

it('should run tests repeatedly', async () => {

161-

await testWatch({ file: 'test.js', fileToUpdate: 'test.js', isolation });

12+

await testRunnerWatch({ file: 'test.js', fileToUpdate: 'test.js', isolation });

16213

});

1631416415

it('should run tests with dependency repeatedly', async () => {

165-

await testWatch({ file: 'test.js', fileToUpdate: 'dependency.js', isolation });

16+

await testRunnerWatch({ file: 'test.js', fileToUpdate: 'dependency.js', isolation });

16617

});

1671816819

it('should run tests with ESM dependency', async () => {

169-

await testWatch({ file: 'test.js', fileToUpdate: 'dependency.mjs', isolation });

20+

await testRunnerWatch({ file: 'test.js', fileToUpdate: 'dependency.mjs', isolation });

17021

});

1712217223

it('should support running tests without a file', async () => {

173-

await testWatch({ fileToUpdate: 'test.js', isolation });

24+

await testRunnerWatch({ fileToUpdate: 'test.js', isolation });

17425

});

1752617627

it('should support a watched test file rename', async () => {

177-

await testWatch({ fileToUpdate: 'test.js', action: 'rename', isolation });

28+

await testRunnerWatch({ fileToUpdate: 'test.js', action: 'rename', isolation });

17829

});

1793018031

it('should not throw when delete a watched test file', async () => {

181-

await testWatch({ fileToUpdate: 'test.js', action: 'delete', isolation });

32+

await testRunnerWatch({ fileToUpdate: 'test.js', action: 'delete', isolation });

18233

});

1833418435

it('should run new tests when a new file is created in the watched directory', {

18536

todo: isolation === 'none' ?

18637

'This test is failing when isolation is set to none and must be fixed' :

18738

undefined,

18839

}, async () => {

189-

await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js', isolation });

40+

await testRunnerWatch({ action: 'create', fileToCreate: 'new-test-file.test.js', isolation });

19041

});

19142

});

19243

}