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..59308f36eae --- /dev/null +++ b/eslint/data-test-selectors.cjs @@ -0,0 +1,28 @@ +'use strict'; + +// `no-restricted-syntax` selectors that flag `data-test-*` CSS/DOM selectors +// in app code. `data-test-*` is a test-only hook, not a functional selector +// API: host app builds strip these attributes in production (ember-test- +// selectors), and in realm-card code (compiled by runtime-common, which does +// NOT strip them) coupling styling or behavior to a test hook is fragile — +// deleting a test selector would silently change production. Either way, use a +// plain `data-*` attribute for things you actually select on. +// +// 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_MESSAGE = + "Don't select on `data-test-*`: it's a test-only attribute (host builds strip it in production; card code keeps it but coupling to a test hook is fragile). Use a plain `data-*` attribute (e.g. `[data-foo]`) for functional selectors."; + +const DATA_TEST_SELECTORS = [ + { + selector: 'Literal[value=/\\[data-test-/]', + message: DATA_TEST_MESSAGE, + }, + { + selector: 'TemplateElement[value.raw=/\\[data-test-/]', + message: DATA_TEST_MESSAGE, + }, +]; + +module.exports = { DATA_TEST_SELECTORS }; diff --git a/packages/base/default-templates/card-info.gts b/packages/base/default-templates/card-info.gts index 2fedb47cdb8..d4ddb35e489 100644 --- a/packages/base/default-templates/card-info.gts +++ b/packages/base/default-templates/card-info.gts @@ -167,6 +167,7 @@ class CardInfoEditor extends GlimmerComponent { (getFieldIcon @model item.key) }} data-test-edit-preview={{item.key}} + data-edit-preview-field={{item.key}} > {{#if item.value}} @@ -200,7 +201,8 @@ class CardInfoEditor extends GlimmerComponent { {{on 'click' this.toggleThumbnailEditor}} data-test-toggle-thumbnail-editor > - Change Thumbnail + Change + {{#unless @hideThemeChooser}}Theme & {{/unless}}Thumbnail @@ -240,10 +242,7 @@ class CardInfoEditor extends GlimmerComponent { @icon={{ImageIcon}} data-test-field='cardInfo-thumbnailURL' > -
+
{ .null-preview { color: var(--muted-foreground, var(--boxel-450)); } - .default-preview :deep([data-test-edit-preview='cardThumbnailURL']) { + .default-preview :deep([data-edit-preview-field='cardThumbnailURL']) { overflow-wrap: anywhere; min-width: 0; } 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..9220db3d6cc 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,22 @@ 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-*` is a test-only hook. Unlike host app builds, the realm card +// pipeline (runtime-common) does NOT strip these attributes, so card selectors +// on them survive to production — but coupling styling/behavior to a test hook +// is fragile (deleting a test selector silently changes production), so it is +// banned in card content too. 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 +77,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/experiments-realm/product.gts b/packages/experiments-realm/product.gts index 0d6d884d7fc..00d538004d9 100644 --- a/packages/experiments-realm/product.gts +++ b/packages/experiments-realm/product.gts @@ -320,9 +320,6 @@ class Isolated extends Component { padding: 7px 24px; border: 0; } - div[data-test-compound-field-format='atom'] { - display: inline-block; - } } @@ -348,9 +345,7 @@ export class Product extends CardDef { }); static embedded = class Embedded extends Component { - + }; static isolated = Isolated; 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.cjs b/packages/software-factory/.template-lintrc.cjs new file mode 100644 index 00000000000..53fd30a5be3 --- /dev/null +++ b/packages/software-factory/.template-lintrc.cjs @@ -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}}