first commit
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled

This commit is contained in:
2026-04-22 19:51:20 +07:00
commit 93d1b7c3d3
579 changed files with 99797 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
import eslint from '@eslint/js';
import ghostPlugin from 'eslint-plugin-ghost';
import playwrightPlugin from 'eslint-plugin-playwright';
import tseslint from 'typescript-eslint';
import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths'
const resetEnvironmentStaleFixtures = ['baseURL', 'ghostAccountOwner', 'page', 'pageWithAuthenticatedUser'];
function isBeforeEachHookCall(node) {
if (node.type !== 'CallExpression') {
return false;
}
if (node.callee.type === 'Identifier') {
return node.callee.name === 'beforeEach';
}
return node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'beforeEach';
}
function isFunctionNode(node) {
return node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'FunctionDeclaration';
}
function getDestructuredFixtureNames(functionNode) {
const [firstParam] = functionNode.params;
if (!firstParam || firstParam.type !== 'ObjectPattern') {
return new Set();
}
const fixtureNames = new Set();
for (const property of firstParam.properties) {
if (property.type !== 'Property') {
continue;
}
if (property.key.type === 'Identifier') {
fixtureNames.add(property.key.name);
}
}
return fixtureNames;
}
const noUnsafeResetEnvironment = {
meta: {
type: 'problem',
docs: {
description: 'Restrict resetEnvironment() to supported beforeEach hooks'
},
messages: {
invalidLocation: 'resetEnvironment() is only supported inside beforeEach hooks. Use a beforeEach hook or switch the file to usePerTestIsolation().',
invalidFixtures: 'Do not resolve {{fixtures}} in the same beforeEach hook as resetEnvironment(); those fixtures become stale after a recycle.'
}
},
create(context) {
return {
CallExpression(node) {
if (isBeforeEachHookCall(node)) {
const callback = node.arguments.find(argument => isFunctionNode(argument));
if (!callback) {
return;
}
const fixtureNames = getDestructuredFixtureNames(callback);
if (!fixtureNames.has('resetEnvironment')) {
return;
}
const staleFixtures = resetEnvironmentStaleFixtures.filter(fixtureName => fixtureNames.has(fixtureName));
if (staleFixtures.length > 0) {
context.report({
node: callback,
messageId: 'invalidFixtures',
data: {
fixtures: staleFixtures.map(fixtureName => `"${fixtureName}"`).join(', ')
}
});
}
return;
}
if (node.callee.type !== 'Identifier' || node.callee.name !== 'resetEnvironment') {
return;
}
const ancestors = context.sourceCode.getAncestors(node);
const enclosingBeforeEachHook = [...ancestors]
.reverse()
.find((ancestor) => isFunctionNode(ancestor) &&
ancestor.parent &&
isBeforeEachHookCall(ancestor.parent));
if (!enclosingBeforeEachHook) {
context.report({
node,
messageId: 'invalidLocation'
});
}
}
};
}
};
const localPlugin = {
rules: {
'no-unsafe-reset-environment': noUnsafeResetEnvironment
}
};
export default tseslint.config([
// Ignore patterns
{
ignores: [
'build/**',
'data/**',
'playwright/**',
'playwright-report/**',
'test-results/**'
]
},
// Base config for all TypeScript files
{
files: ['**/*.ts', '**/*.mjs'],
extends: [
eslint.configs.recommended,
tseslint.configs.recommended
],
languageOptions: {
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
},
plugins: {
ghost: ghostPlugin,
playwright: playwrightPlugin,
'no-relative-import-paths': noRelativeImportPaths,
local: localPlugin,
},
rules: {
// Manually include rules from plugin:ghost/ts and plugin:ghost/ts-test
// These would normally come from the extends, but flat config requires explicit inclusion
...ghostPlugin.configs.ts.rules,
// Sort multiple import lines into alphabetical groups
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
}],
// Enforce kebab-case (lowercase with hyphens) for all filenames
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],
// Apply no-relative-import-paths rule
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ allowSameFolder: true, rootDir: './', prefix: '@' },
],
// Restrict imports to specific directories
'no-restricted-imports': ['error', {
patterns: ['@/helpers/pages/*']
}],
// Disable all mocha rules from ghost plugin since this package uses playwright instead
...Object.fromEntries(
Object.keys(ghostPlugin.rules || {})
.filter(rule => rule.startsWith('mocha/'))
.map(rule => [`ghost/${rule}`, 'off'])
)
}
},
// Keep assertions in test files and Playwright-specific helpers.
{
files: ['**/*.ts', '**/*.mjs'],
ignores: [
'tests/**/*.ts',
'helpers/playwright/**/*.ts',
'visual-regression/**/*.ts'
],
rules: {
'no-restricted-syntax': ['error',
{
selector: "ImportSpecifier[imported.name='expect'][parent.source.value='@playwright/test']",
message: 'Keep Playwright expect assertions in test files.'
},
{
selector: "ImportSpecifier[imported.name='expect'][parent.source.value='@/helpers/playwright']",
message: 'Keep Playwright expect assertions in test files.'
}
]
}
},
// Playwright-specific recommended rules config for test files
{
files: ['tests/**/*.ts', 'helpers/playwright/**/*.ts', 'helpers/pages/**/*.ts'],
rules: {
...playwrightPlugin.configs.recommended.rules,
'playwright/expect-expect': ['warn', {
assertFunctionPatterns: ['^expect[A-Z].*']
}]
}
},
// Keep test files on page objects and the supported isolation APIs.
{
files: ['tests/**/*.ts'],
rules: {
'local/no-unsafe-reset-environment': 'error',
'no-restricted-syntax': ['error',
{
selector: "CallExpression[callee.object.name='page'][callee.property.name='locator']",
message: 'Use page objects or higher-level page methods instead of page.locator() in test files.'
},
{
selector: 'MemberExpression[object.property.name="describe"][property.name="parallel"]',
message: 'test.describe.parallel() is deprecated. Use usePerTestIsolation() from @/helpers/playwright/isolation instead.'
},
{
selector: 'MemberExpression[object.property.name="describe"][property.name="serial"]',
message: 'test.describe.serial() is deprecated. Use test.describe.configure({mode: "serial"}) if needed.'
}
]
}
}
]);