Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

/** @typedef {import("eslint").Rule.RuleModule} Rule */
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.CallExpression} CallExpression */

const path = require('path');

const ERROR_MSG_MISSING_HOOK =
'`global.setup.ts` must explicitly call `globalSetupHook`. Without it, ES security indexes are not pre-generated and tests become flaky.';

/**
* Checks if a file is a global.setup.ts file
* @param {string} filename
* @returns {boolean}
*/
const isGlobalSetupFile = (filename) => {
return path.basename(filename) === 'global.setup.ts';
};

/**
* Checks if a node is a call to globalSetupHook
* @param {CallExpression} node
* @returns {boolean}
*/
const isGlobalSetupHookCall = (node) => {
return node.callee.type === 'Identifier' && node.callee.name === 'globalSetupHook';
};

/** @type {Rule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Require globalSetupHook call in global.setup.ts files',
category: 'Best Practices',
},
fixable: null,
schema: [],
},
create: (context) => {
const filename = context.getFilename();

if (!isGlobalSetupFile(filename)) {
return {};
}

let hasGlobalSetupHook = false;

return {
CallExpression(node) {
if (isGlobalSetupHookCall(node)) {
hasGlobalSetupHook = true;
}
},
'Program:exit'(node) {
if (!hasGlobalSetupHook) {
context.report({
node,
message: ERROR_MSG_MISSING_HOOK,
});
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

const { RuleTester } = require('eslint');
const rule = require('./scout_require_global_setup_hook_in_parallel_tests');
const dedent = require('dedent');

const ERROR_MSG_MISSING_HOOK =
'`global.setup.ts` must explicitly call `globalSetupHook`. Without it, ES security indexes are not pre-generated and tests become flaky.';

const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
},
});

ruleTester.run('@kbn/eslint/scout_require_global_setup_hook_in_parallel_tests', rule, {
valid: [
// global.setup.ts with globalSetupHook call
{
code: dedent`
import { globalSetupHook } from '@kbn/scout-security';

globalSetupHook('Ingest archives', async ({ esArchiver }) => {});
`,
filename: '/path/to/plugin/test/scout/ui/parallel_tests/global.setup.ts',
},
// Non global.setup.ts file (rule should not apply)
{
code: `export const setup = () => {};`,
filename: '/path/to/plugin/test/scout/ui/some_other_file.ts',
},
],

invalid: [
// globalSetupHook not present in global.setup.ts
{
code: `export const setup = async () => {};`,
filename: '/path/to/plugin/test/scout/ui/parallel_tests/global.setup.ts',
errors: [{ message: ERROR_MSG_MISSING_HOOK }],
},
// globalSetupHook imported but not called
{
code: dedent`
import { globalSetupHook } from '@kbn/scout-security';

export const setup = async () => {};
`,
filename: '/path/to/plugin/test/scout/ui/parallel_tests/global.setup.ts',
errors: [{ message: ERROR_MSG_MISSING_HOOK }],
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function createPlaywrightConfig(options: ScoutPlaywrightOptions): Playwri
testIdAttribute: 'data-test-subj',
serversConfigDir: SCOUT_SERVERS_ROOT,
[VALID_CONFIG_MARKER]: true,
runGlobalSetup: options.runGlobalSetup,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,60 @@ describe('validatePlaywrightConfig', () => {
`Path to a valid TypeScript config file is required: --config <relative path to .ts file>`
);
});

it('should throw an error if runGlobalSetup is true but global.setup.ts does not exist', async () => {
const configPath = '/path/to/plugin/playwright.config.ts';
existsSyncMock
.mockReturnValueOnce(true) // config file exists
.mockReturnValueOnce(false); // global.setup.ts does not exist
loadConfigModuleMock.mockResolvedValue({
default: {
use: { [VALID_CONFIG_MARKER]: true, runGlobalSetup: true },
testDir: './tests',
},
});

await expect(validatePlaywrightConfig(configPath)).rejects.toThrow(
`The config file at "${configPath}" has 'runGlobalSetup: true' but no global.setup.ts file found.`
);
});

it('should pass validation if runGlobalSetup is true and global.setup.ts exists', async () => {
const configPath = '/path/to/plugin/playwright.config.ts';
existsSyncMock.mockReturnValue(true); // both config and global.setup.ts exist
loadConfigModuleMock.mockResolvedValue({
default: {
use: { [VALID_CONFIG_MARKER]: true, runGlobalSetup: true },
testDir: './tests',
},
});

await expect(validatePlaywrightConfig(configPath)).resolves.not.toThrow();
});

it('should pass validation if runGlobalSetup is false', async () => {
const configPath = '/path/to/plugin/playwright.config.ts';
existsSyncMock.mockReturnValue(true);
loadConfigModuleMock.mockResolvedValue({
default: {
use: { [VALID_CONFIG_MARKER]: true, runGlobalSetup: false },
testDir: './tests',
},
});

await expect(validatePlaywrightConfig(configPath)).resolves.not.toThrow();
});

it('should pass validation if runGlobalSetup is undefined', async () => {
const configPath = '/path/to/plugin/playwright.config.ts';
existsSyncMock.mockReturnValue(true);
loadConfigModuleMock.mockResolvedValue({
default: {
use: { [VALID_CONFIG_MARKER]: true },
testDir: './tests',
},
});

await expect(validatePlaywrightConfig(configPath)).resolves.not.toThrow();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
*/

import Fs from 'fs';
import { PlaywrightTestConfig } from 'playwright/test';
import path from 'path';
import type { PlaywrightTestConfig } from 'playwright/test';
import { createFlagError } from '@kbn/dev-cli-errors';
import { ScoutTestOptions, VALID_CONFIG_MARKER } from '../types';
import { loadConfigModule } from './config_loader';
Expand Down Expand Up @@ -44,4 +45,19 @@ export async function validatePlaywrightConfig(configPath: string) {
`The config file at "${configPath}" must export a valid Playwright configuration with "testDir" property.`
);
}

if (config.use?.runGlobalSetup) {
const configDir = path.dirname(configPath);
const testDir = path.resolve(configDir, config.testDir);
const globalSetupPath = path.join(testDir, 'global.setup.ts');
const hasGlobalSetupFile = Fs.existsSync(globalSetupPath);

if (!hasGlobalSetupFile) {
throw createFlagError(
`The config file at "${configPath}" has 'runGlobalSetup: true' but no global.setup.ts file found.\n` +
`Expected file at: ${globalSetupPath}\n\n` +
`Either create the file or set 'runGlobalSetup: false' in your config.`
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ScoutTestOptions extends PlaywrightTestOptions {
serversConfigDir: string;
configName: ScoutConfigName;
[VALID_CONFIG_MARKER]: boolean;
runGlobalSetup?: boolean;
}

export interface ScoutPlaywrightOptions extends Pick<PlaywrightTestConfig, 'testDir' | 'workers'> {
Expand Down