From 6130b946c333be4322f16a089ece6dfcd87f0ffc Mon Sep 17 00:00:00 2001 From: Burcu Noyan Date: Fri, 26 Jun 2026 15:14:18 -0400 Subject: [PATCH 1/4] fix linting for uncaught data-test-* usage --- .eslintrc.js | 14 +- eslint/data-test-selectors.cjs | 24 + packages/boxel-ui/addon/.eslintrc.cjs | 15 + packages/boxel-ui/test-app/.eslintrc.js | 15 + packages/catalog/.eslintrc.cjs | 29 +- packages/host/.eslintrc.js | 13 + packages/software-factory/.template-lintrc.js | 6 + packages/software-factory/package.json | 3 + packages/software-factory/realm/document.gts | 2 +- .../software-factory/realm/test-results.gts | 4 +- .../lib/no-data-test-selector.mjs | 19 + packages/template-lint/plugin.mjs | 16 + pnpm-lock.yaml | 467 +++++++++--------- 13 files changed, 376 insertions(+), 251 deletions(-) create mode 100644 eslint/data-test-selectors.cjs create mode 100644 packages/software-factory/.template-lintrc.js diff --git a/.eslintrc.js b/.eslintrc.js index 258ccc22af4..44de9824bc8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,19 +4,7 @@ const { NO_COMPILATION_REQUIRED_TS_SELECTORS, CJS_GLOBALS_IN_ESM, } = require('./eslint/erasable-syntax-selectors.cjs'); - -const DATA_TEST_SELECTORS = [ - { - selector: 'Literal[value=/\\[data-test-/]', - message: - '`data-test-*` attributes are stripped in production builds. Use a plain `data-*` attribute (e.g. `[data-foo]`) for functional selectors.', - }, - { - selector: 'TemplateElement[value.raw=/\\[data-test-/]', - message: - '`data-test-*` attributes are stripped in production builds. Use a plain `data-*` attribute (e.g. `[data-foo]`) for functional selectors.', - }, -]; +const { DATA_TEST_SELECTORS } = require('./eslint/data-test-selectors.cjs'); module.exports = { root: true, diff --git a/eslint/data-test-selectors.cjs b/eslint/data-test-selectors.cjs new file mode 100644 index 00000000000..b45c27f56b0 --- /dev/null +++ b/eslint/data-test-selectors.cjs @@ -0,0 +1,24 @@ +'use strict'; + +// `no-restricted-syntax` selectors that flag `data-test-*` CSS/DOM selectors +// in app code. ember-test-selectors strips these attributes from production +// builds, so selectors like `querySelector('[data-test-foo]')` work in tests +// but silently break everywhere else. +// +// Shared by the root `.eslintrc.js` and by packages whose own config is +// `root: true` (e.g. `packages/host`, `packages/boxel-ui/addon`), which do not +// inherit the root config and so must re-declare these selectors themselves. +const DATA_TEST_SELECTORS = [ + { + selector: 'Literal[value=/\\[data-test-/]', + message: + '`data-test-*` attributes are stripped in production builds. Use a plain `data-*` attribute (e.g. `[data-foo]`) for functional selectors.', + }, + { + selector: 'TemplateElement[value.raw=/\\[data-test-/]', + message: + '`data-test-*` attributes are stripped in production builds. Use a plain `data-*` attribute (e.g. `[data-foo]`) for functional selectors.', + }, +]; + +module.exports = { DATA_TEST_SELECTORS }; diff --git a/packages/boxel-ui/addon/.eslintrc.cjs b/packages/boxel-ui/addon/.eslintrc.cjs index 31485b410a5..f970070f1f3 100644 --- a/packages/boxel-ui/addon/.eslintrc.cjs +++ b/packages/boxel-ui/addon/.eslintrc.cjs @@ -1,6 +1,9 @@ 'use strict'; const MISSING_INVOKABLES_CONFIG = require('../../runtime-common/etc/eslint/missing-invokables-config'); +const { + DATA_TEST_SELECTORS, +} = require('../../../eslint/data-test-selectors.cjs'); module.exports = { root: true, @@ -45,6 +48,18 @@ module.exports = { ], }, overrides: [ + { + // Disallow `data-test-*` CSS/DOM selectors in source code. ember-test-selectors + // strips these attributes in production, so selectors like + // `querySelector('[data-test-foo]')` silently break outside of tests. + // This package is `root: true`, so it cannot inherit the monorepo-root + // config and must re-declare the guard. Scoped to source only — tests + // legitimately select on `data-test-*`. + files: ['src/**/*.{js,ts,gts,gjs}'], + rules: { + 'no-restricted-syntax': ['error', ...DATA_TEST_SELECTORS], + }, + }, { files: ['**/*.gts'], parser: 'ember-eslint-parser', diff --git a/packages/boxel-ui/test-app/.eslintrc.js b/packages/boxel-ui/test-app/.eslintrc.js index b28976b1b61..75a098ade06 100644 --- a/packages/boxel-ui/test-app/.eslintrc.js +++ b/packages/boxel-ui/test-app/.eslintrc.js @@ -1,6 +1,9 @@ 'use strict'; const MISSING_INVOKABLES_CONFIG = require('../../runtime-common/etc/eslint/missing-invokables-config'); +const { + DATA_TEST_SELECTORS, +} = require('../../../eslint/data-test-selectors.cjs'); module.exports = { root: true, @@ -43,6 +46,18 @@ module.exports = { ], }, overrides: [ + { + // Disallow `data-test-*` CSS/DOM selectors in app code. ember-test-selectors + // strips these attributes in production, so selectors like + // `querySelector('[data-test-foo]')` silently break outside of tests. + // This package is `root: true`, so it cannot inherit the monorepo-root + // config and must re-declare the guard. Scoped to app code only — tests + // legitimately select on `data-test-*`. + files: ['app/**/*.{js,ts,gts,gjs}'], + rules: { + 'no-restricted-syntax': ['error', ...DATA_TEST_SELECTORS], + }, + }, { files: ['**/*.gts'], parser: 'ember-eslint-parser', diff --git a/packages/catalog/.eslintrc.cjs b/packages/catalog/.eslintrc.cjs index 2483b2c8c29..7132853598f 100644 --- a/packages/catalog/.eslintrc.cjs +++ b/packages/catalog/.eslintrc.cjs @@ -3,6 +3,7 @@ const { NO_COMPILATION_REQUIRED_TS_SELECTORS, } = require('../../eslint/erasable-syntax-selectors.cjs'); +const { DATA_TEST_SELECTORS } = require('../../eslint/data-test-selectors.cjs'); // contents/ files (both .ts card/command modules and .gts Glimmer components) // always go through the realm/Ember compilation pipeline, so decorators like @@ -10,12 +11,19 @@ const { // is lifted. The remaining erasable-syntax guards (enum, `import =`, // `export =`, runtime namespaces) still apply, for consistency with the rest // of the repo. +const ERASABLE_MINUS_DECORATOR = NO_COMPILATION_REQUIRED_TS_SELECTORS.filter( + (s) => s.selector !== 'Decorator', +); + +// `data-test-*` selectors are stripped from production builds, so they are +// banned in card content too — but tests legitimately select on them, so the +// data-test ban is dropped for test files (see CONTENTS_TEST_RESTRICTED_SYNTAX). const CONTENTS_RESTRICTED_SYNTAX = [ 'error', - ...NO_COMPILATION_REQUIRED_TS_SELECTORS.filter( - (s) => s.selector !== 'Decorator', - ), + ...ERASABLE_MINUS_DECORATOR, + ...DATA_TEST_SELECTORS, ]; +const CONTENTS_TEST_RESTRICTED_SYNTAX = ['error', ...ERASABLE_MINUS_DECORATOR]; module.exports = { overrides: [ @@ -66,5 +74,20 @@ module.exports = { 'no-undef': 'off', }, }, + { + // Tests legitimately select on `data-test-*` (e.g. + // `assert.dom('[data-test-foo]')`); keep the erasable-syntax guards but + // drop the data-test ban. This override only resets `no-restricted-syntax` + // — the parser/extends from the `contents/**/*.gts` override above still + // apply to `.gts` tests. + files: [ + 'contents/**/*.test.{js,ts,gts,gjs}', + 'contents/**/*-test.{js,ts,gts,gjs}', + 'contents/tests/**', + ], + rules: { + 'no-restricted-syntax': CONTENTS_TEST_RESTRICTED_SYNTAX, + }, + }, ], }; diff --git a/packages/host/.eslintrc.js b/packages/host/.eslintrc.js index da191180a60..4c3900a7be6 100644 --- a/packages/host/.eslintrc.js +++ b/packages/host/.eslintrc.js @@ -1,6 +1,7 @@ 'use strict'; const MISSING_INVOKABLES_CONFIG = require('../runtime-common/etc/eslint/missing-invokables-config'); +const { DATA_TEST_SELECTORS } = require('../../eslint/data-test-selectors.cjs'); // Applies to all of JS, TS, GJS, and GTS in the browser context. const sharedBrowserConfig = { @@ -59,6 +60,18 @@ module.exports = { browser: true, }, overrides: [ + { + // Disallow `data-test-*` CSS/DOM selectors in app code. ember-test-selectors + // strips these attributes in production, so selectors like + // `querySelector('[data-test-foo]')` silently break outside of tests. + // This package is `root: true`, so it cannot inherit the monorepo-root + // config and must re-declare the guard. Scoped to app code only — tests + // legitimately select on `data-test-*`. + files: ['app/**/*.{js,ts,gts,gjs}'], + rules: { + 'no-restricted-syntax': ['error', ...DATA_TEST_SELECTORS], + }, + }, { files: ['**/*.{js,ts}'], parser: '@typescript-eslint/parser', diff --git a/packages/software-factory/.template-lintrc.js b/packages/software-factory/.template-lintrc.js new file mode 100644 index 00000000000..53fd30a5be3 --- /dev/null +++ b/packages/software-factory/.template-lintrc.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + extends: ['recommended', '@cardstack/template-lint:recommended'], + plugins: ['../template-lint/plugin'], +}; diff --git a/packages/software-factory/package.json b/packages/software-factory/package.json index 9cb479890ec..fa57bfe70a9 100644 --- a/packages/software-factory/package.json +++ b/packages/software-factory/package.json @@ -16,6 +16,8 @@ "smoke:tools": "NODE_NO_WARNINGS=1 node scripts/smoke-tests/factory-tools-smoke.ts", "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"", "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"", + "lint:hbs": "ember-template-lint realm --no-error-on-unmatched-pattern", + "lint:hbs:fix": "ember-template-lint realm --fix --no-error-on-unmatched-pattern", "lint:js": "eslint . --report-unused-disable-directives --cache", "lint:js:fix": "eslint . --report-unused-disable-directives --fix", "lint:format": "prettier --check .", @@ -62,6 +64,7 @@ "decorator-transforms": "catalog:", "ember-concurrency": "catalog:", "ember-modifier": "^4.1.0", + "ember-template-lint": "catalog:", "eslint-plugin-qunit": "catalog:", "opencode-ai": "1.14.34", "fs-extra": "catalog:", diff --git a/packages/software-factory/realm/document.gts b/packages/software-factory/realm/document.gts index c0bd4a34e26..5a71eabf393 100644 --- a/packages/software-factory/realm/document.gts +++ b/packages/software-factory/realm/document.gts @@ -539,7 +539,7 @@ export class Document extends CardDef { - {{! template-lint-disable no-invalid-interactive*/}} + {{! template-lint-disable no-invalid-interactive}}