diff --git a/.eslintrc.js b/.eslintrc.js index eb59f2ad..bce5934c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,7 +55,6 @@ module.exports = { './.eslintrc.js', './.prettierrc.js', './.stylelintrc.js', - './.template-lintrc.js', './ember-cli-build.js', './index.js', './testem.js', @@ -74,5 +73,18 @@ module.exports = { }, extends: ['plugin:n/recommended'], }, + // QUnit test files. `no-hooks-from-ancestor-modules` is the one that matters most here: + // a nested `module('…', function (hooks) { … })` shadows the outer hooks, which QUnit 3 + // turns into a hard error. It is easy to reintroduce and the suite stays green when you do. + { + files: ['tests/**/*-test.js'], + plugins: ['qunit'], + extends: ['plugin:qunit/recommended'], + rules: { + // Opinionated and noisy for this suite: most tests assert an obvious, fixed number + // of things, and an expect() count that drifts is worse than none. + 'qunit/require-expect': 'off', + }, + }, ], }; diff --git a/.template-lintrc.js b/.template-lintrc.mjs similarity index 54% rename from .template-lintrc.js rename to .template-lintrc.mjs index c3086c51..dc564205 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.mjs @@ -1,8 +1,20 @@ -'use strict'; +import NoUnguardedHandlerArgument from './lint/no-unguarded-handler-argument.mjs'; -module.exports = { +export default { + plugins: [ + { + name: 'fleetbase-ember-ui', + rules: { + 'no-unguarded-handler-argument': NoUnguardedHandlerArgument, + }, + }, + ], extends: 'recommended', rules: { + // `{{on "click" @arg}}` and `{{fn @arg …}}` THROW while rendering when the argument is + // absent — they are not no-ops. Thirteen bindings across five components were fixed for + // this; the rule keeps them from coming back. + 'no-unguarded-handler-argument': true, 'no-invalid-interactive': 'off', 'no-yield-only': 'off', 'no-pointer-down-event-binding': 'off', @@ -18,6 +30,15 @@ module.exports = { }, }, overrides: [ + { + // Test fixtures always supply the handlers they bind — the template is written + // alongside the arguments it needs. The rule exists to protect the addon's shipped + // components, where the caller is someone else. + files: ['tests/**/*-test.js'], + rules: { + 'no-unguarded-handler-argument': false, + }, + }, { // Modifier tests set inline styles on their fixtures because the // element's own style is exactly what the modifier under test reads diff --git a/addon/components/layout/header/dropdown/item.hbs b/addon/components/layout/header/dropdown/item.hbs index a6f0b35c..0c863812 100644 --- a/addon/components/layout/header/dropdown/item.hbs +++ b/addon/components/layout/header/dropdown/item.hbs @@ -74,7 +74,7 @@ class="next-header-dd-menu-item next-dd-item {{if @item.disabled 'disabled'}} {{@item.class}}" target={{@item.target}} disabled={{@item.disabled}} - {{on "click" (fn @onAction @item.action @item.params)}} + {{on "click" (fn (or @onAction (noop)) @item.action @item.params)}} > {{#if @item.icon}} @@ -90,7 +90,7 @@ {{#if this.isInteractive}}
- + {{#if @item.icon}} {{/if}} diff --git a/addon/components/modals/bulk-action-model.hbs b/addon/components/modals/bulk-action-model.hbs index c43cf5bd..124be9ab 100644 --- a/addon/components/modals/bulk-action-model.hbs +++ b/addon/components/modals/bulk-action-model.hbs @@ -53,7 +53,7 @@
- +
diff --git a/addon/components/modals/bulk-delete-model.hbs b/addon/components/modals/bulk-delete-model.hbs index 552b3f86..7be29929 100644 --- a/addon/components/modals/bulk-delete-model.hbs +++ b/addon/components/modals/bulk-delete-model.hbs @@ -30,7 +30,7 @@
  • {{get selected @options.modelNamePath}} - +
  • diff --git a/addon/components/modals/import-form.hbs b/addon/components/modals/import-form.hbs index c91a4c69..49afd934 100644 --- a/addon/components/modals/import-form.hbs +++ b/addon/components/modals/import-form.hbs @@ -79,7 +79,7 @@ {{#each @options.fileQueueColumns as |column|}} {{#if (eq column.key "delete")}} - + {{else if (eq column.key "type")}} diff --git a/addon/components/modals/save-report.hbs b/addon/components/modals/save-report.hbs index dd4f243f..eec4e751 100644 --- a/addon/components/modals/save-report.hbs +++ b/addon/components/modals/save-report.hbs @@ -15,7 +15,7 @@ {{#if @options.showScheduling}}
    - + diff --git a/addon/components/overlay/header.hbs b/addon/components/overlay/header.hbs index 7ccb9867..1caf2f36 100644 --- a/addon/components/overlay/header.hbs +++ b/addon/components/overlay/header.hbs @@ -113,7 +113,7 @@ class="next-content-overlay-panel-maximize-button {{@maximizeButtonClass}}" {{set-height @maximizeButtonHeight}} {{set-width @maximizeButtonWidth}} - {{on "click" @overlay.maximize}} + {{on "click" (or @overlay.maximize (noop))}} > @@ -124,7 +124,7 @@ class="next-content-overlay-panel-minimize-button {{@minimizeButtonClass}}" {{set-height @minimizeButtonHeight}} {{set-width @minimizeButtonWidth}} - {{on "click" @overlay.minimize}} + {{on "click" (or @overlay.minimize (noop))}} > diff --git a/addon/components/table/cell/link-list.hbs b/addon/components/table/cell/link-list.hbs index a5315aea..ec87361d 100644 --- a/addon/components/table/cell/link-list.hbs +++ b/addon/components/table/cell/link-list.hbs @@ -3,7 +3,7 @@ {{#let (get @row @column.valuePath) as |items|}} {{#each items as |item|}}
  • - + {{get item (or @column.cellComponentLabelPath "name")}}
  • diff --git a/lint/no-unguarded-handler-argument.mjs b/lint/no-unguarded-handler-argument.mjs new file mode 100644 index 00000000..cc8ffb8c --- /dev/null +++ b/lint/no-unguarded-handler-argument.mjs @@ -0,0 +1,104 @@ +import { Rule } from 'ember-template-lint'; + +/** + * Flags `{{on "event" @handler}}` and `{{fn @handler …}}` where the handler position holds a BARE + * argument. + * + * These are not no-ops when the argument is absent — Glimmer throws while rendering: + * + * {{on "click" @onFoo}} → "You must pass a function as the second argument to the `on` modifier" + * {{fn @onFoo item}} → "You must pass a function as the `fn` helper's first argument" + * + * so a component with an unguarded binding cannot be rendered at all without that argument. Five + * components in this addon were untestable for exactly this reason. + * + * Guarded forms are accepted: + * {{on "click" (or @onFoo (noop))}} — the pattern this codebase settled on + * {{#if @item.onClick}}{{on "click" @item.onClick}}{{/if}} + * {{on "click" this.handler}} — a component's own action always exists + */ +export default class NoUnguardedHandlerArgument extends Rule { + logNode({ node, message }) { + this.log({ message, node }); + } + + /** Is this node a bare `@arg` / `@arg.path` reference? */ + isBareArgument(node) { + return node && node.type === 'PathExpression' && node.head && node.head.type === 'AtHead'; + } + + /** Walk out to see whether an enclosing `{{#if}}`/`{{#unless}}` already tests this argument. */ + isGuardedByBlock(argumentName) { + return this.guardStack.some((guard) => guard === argumentName || argumentName.startsWith(`${guard}.`)); + } + + static pathName(node) { + return node.original || ''; + } + + visitor() { + this.guardStack = []; + + const enterBlock = (node) => { + const name = node.path && node.path.original; + if (name !== 'if' && name !== 'unless') { + return; + } + const [condition] = node.params; + if (this.isBareArgument(condition)) { + this.guardStack.push(NoUnguardedHandlerArgument.pathName(condition)); + } else { + this.guardStack.push(null); + } + }; + + const exitBlock = (node) => { + const name = node.path && node.path.original; + if (name === 'if' || name === 'unless') { + this.guardStack.pop(); + } + }; + + const check = (node, position, describe) => { + const candidate = node.params[position]; + if (!this.isBareArgument(candidate)) { + return; + } + const argumentName = NoUnguardedHandlerArgument.pathName(candidate); + if (this.isGuardedByBlock(argumentName)) { + return; + } + this.logNode({ + node: candidate, + message: + `${describe} receives the bare argument \`${argumentName}\`, which throws while rendering when the ` + + `argument is absent. Guard it — \`(or ${argumentName} (noop))\` — or wrap the binding in ` + + `\`{{#if ${argumentName}}}\`.`, + }); + }; + + return { + BlockStatement: { enter: enterBlock, exit: exitBlock }, + + ElementModifierStatement(node) { + // {{on "click" @onFoo}} — the handler is the second parameter + if (node.path && node.path.original === 'on' && node.params.length >= 2) { + check(node, 1, 'The `on` modifier'); + } + }, + + SubExpression(node) { + // (fn @onFoo item) — the function is the first parameter + if (node.path && node.path.original === 'fn' && node.params.length >= 1) { + check(node, 0, 'The `fn` helper'); + } + }, + + MustacheStatement(node) { + if (node.path && node.path.original === 'fn' && node.params.length >= 1) { + check(node, 0, 'The `fn` helper'); + } + }, + }; + } +}; diff --git a/package.json b/package.json index 8468aa34..46607788 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "eslint-plugin-ember": "^11.11.1", "eslint-plugin-n": "^16.2.0", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-qunit": "^8.0.1", + "eslint-plugin-qunit": "^8.2.6", "loader.js": "^4.7.0", "prettier": "^3.0.3", "qunit": "^2.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 987506bb..d3780ea2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,7 +373,7 @@ importers: specifier: ^5.0.1 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.3) eslint-plugin-qunit: - specifier: ^8.0.1 + specifier: ^8.2.6 version: 8.2.6(eslint@8.57.1) loader.js: specifier: ^4.7.0 diff --git a/tests/integration/components/drawer-test.js b/tests/integration/components/drawer-test.js index 3971f521..52ae574a 100644 --- a/tests/integration/components/drawer-test.js +++ b/tests/integration/components/drawer-test.js @@ -198,7 +198,7 @@ module('Integration | Component | drawer', function (hooks) { loaded[0].minimize(); await settled(); - assert.true(loaded[0].isMinimized === false, 'the snapshot is not live; callers must re-read via a fresh callback'); + assert.false(loaded[0].isMinimized, 'the snapshot is not live; callers must re-read via a fresh callback'); assert.dom(drawer()).hasClass('drawer-is-minimized', 'the drawer itself did update'); }); diff --git a/tests/integration/components/dropdown-button-test.js b/tests/integration/components/dropdown-button-test.js index c8d5f4a3..f7c48318 100644 --- a/tests/integration/components/dropdown-button-test.js +++ b/tests/integration/components/dropdown-button-test.js @@ -241,7 +241,7 @@ module('Integration | Component | dropdown-button', function (hooks) { await render(TEMPLATE); - assert.ok(find(`${TRIGGER} .ember-attacher`) || find(TRIGGER), 'the trigger renders with a tooltip attached'); + assert.dom(TRIGGER).exists('the trigger renders when help text is permitted'); }); test('visibility and disabled state follow their arguments', async function (assert) { diff --git a/tests/integration/components/layout/sidebar/navigator-test.js b/tests/integration/components/layout/sidebar/navigator-test.js index 9679c73a..b1b0d909 100644 --- a/tests/integration/components/layout/sidebar/navigator-test.js +++ b/tests/integration/components/layout/sidebar/navigator-test.js @@ -325,17 +325,11 @@ module('Integration | Component | layout/sidebar/navigator', function (hooks) { ], }, ]); + const syncCalls = []; this.set('shouldSyncInitialActiveParent', ({ activePath, routeName, currentURL }) => { - if (routeName === 'console.settings.index') { - assert.deepEqual( - activePath.map((item) => item.label), - ['Settings', 'General'] - ); - assert.strictEqual(currentURL, '/settings'); - return false; - } + syncCalls.push({ labels: activePath.map((item) => item.label), routeName, currentURL }); - return true; + return routeName !== 'console.settings.index'; }); await render(hbs``); @@ -343,6 +337,13 @@ module('Integration | Component | layout/sidebar/navigator', function (hooks) { assert.dom('.next-sidebar-navigator-back').doesNotExist('initial render stays at root when predicate returns false'); assert.dom('.next-sidebar-navigator-view-in').includesText('Settings'); + // Asserted outside the predicate: inside it, a change to the routeName would have skipped + // the branch and quietly asserted nothing. + const settingsCall = syncCalls.find((call) => call.routeName === 'console.settings.index'); + assert.ok(settingsCall, 'the predicate is consulted for the active route'); + assert.deepEqual(settingsCall.labels, ['Settings', 'General'], 'it receives the full active path'); + assert.strictEqual(settingsCall.currentURL, '/settings', 'and the current url'); + const router = this.owner.lookup('service:router'); router.currentRouteName = 'console.settings.security'; router.currentURL = '/settings/security'; diff --git a/tests/integration/components/metadata-editor-test.js b/tests/integration/components/metadata-editor-test.js index 8e503c11..5828f68e 100644 --- a/tests/integration/components/metadata-editor-test.js +++ b/tests/integration/components/metadata-editor-test.js @@ -200,7 +200,7 @@ module('Integration | Component | metadata-editor', function (hooks) { await render(TEMPLATE); await click(rows()[0].querySelector('input[type="checkbox"]')); - assert.strictEqual(changes[changes.length - 1].active, true); + assert.true(changes[changes.length - 1].active); }); test('changing the type coerces the existing value', async function (assert) { @@ -228,7 +228,7 @@ module('Integration | Component | metadata-editor', function (hooks) { await render(TEMPLATE); await fillIn(rows()[0].querySelector('select'), 'boolean'); - assert.strictEqual(changes[changes.length - 1].thing, true); + assert.true(changes[changes.length - 1].thing); }); test('boolean rows are omitted from the output when allowBoolean is false', async function (assert) { diff --git a/tests/integration/components/metadata-viewer-test.js b/tests/integration/components/metadata-viewer-test.js index 6de146c2..eea68a56 100644 --- a/tests/integration/components/metadata-viewer-test.js +++ b/tests/integration/components/metadata-viewer-test.js @@ -55,9 +55,11 @@ module('Integration | Component | metadata-viewer', function (hooks) { const [tags, nested] = rows(); assert.strictEqual(tags[0], 'tags'); - assert.true(tags[1].includes('"a"') && tags[1].includes('"b"'), 'the array is shown as json'); + assert.true(tags[1].includes('"a"'), 'the array is shown as json'); + assert.true(tags[1].includes('"b"'), 'including every element'); assert.strictEqual(nested[0], 'nested'); - assert.true(nested[1].includes('"x"') && nested[1].includes('1'), 'so is the object'); + assert.true(nested[1].includes('"x"'), 'so is the object'); + assert.true(nested[1].includes('1'), 'including its value'); assert.false(nested[1].includes('[object Object]'), 'nothing falls through to a raw stringification'); }); diff --git a/tests/integration/components/money-input-test.js b/tests/integration/components/money-input-test.js index 36dfb500..6da854f2 100644 --- a/tests/integration/components/money-input-test.js +++ b/tests/integration/components/money-input-test.js @@ -130,7 +130,8 @@ module('Integration | Component | money-input', function (hooks) { await render(TEMPLATE); - assert.true(input().value.includes('£') || input().value.includes('25'), 'the amount is formatted for the currency'); + assert.true(input().value.includes('25'), `the amount is present (got ${input().value})`); + assert.true(input().value.includes('£'), `and carries the currency symbol (got ${input().value})`); }); test('a comma-decimal currency uses its own separator', async function (assert) { diff --git a/tests/integration/components/pill-test.js b/tests/integration/components/pill-test.js index 34be015e..93c2988d 100644 --- a/tests/integration/components/pill-test.js +++ b/tests/integration/components/pill-test.js @@ -157,7 +157,8 @@ module('Integration | Component | pill', function (hooks) { `); - assert.ok(find('.tip') || find('.fleetbase-pill'), 'the pill renders with a tooltip attached'); + assert.dom('.fleetbase-pill').exists('the pill renders'); + assert.ok(find('.tip'), 'with a tooltip attached to it'); }); }); diff --git a/tests/integration/components/query-builder/computed-columns-test.js b/tests/integration/components/query-builder/computed-columns-test.js index 79d58c21..16e8dfa4 100644 --- a/tests/integration/components/query-builder/computed-columns-test.js +++ b/tests/integration/components/query-builder/computed-columns-test.js @@ -126,7 +126,7 @@ module('Integration | Component | query-builder/computed-columns', function (hoo const badges = items().map((item) => item.querySelector('.rounded.text-xs').textContent.trim()); assert.deepEqual(badges, ['Text', 'Integer', 'Decimal', 'Date', 'Date & Time', 'Boolean']); - assert.strictEqual(findAll('.computed-column-item svg').length >= 6, true, 'each row carries a type icon'); + assert.true(findAll('.computed-column-item svg').length >= 6, 'each row carries a type icon'); }); test('an unrecognised type falls back to a question icon and no label', async function (assert) { diff --git a/tests/integration/components/query-builder/conditions-test.js b/tests/integration/components/query-builder/conditions-test.js index a21d32fc..525d537d 100644 --- a/tests/integration/components/query-builder/conditions-test.js +++ b/tests/integration/components/query-builder/conditions-test.js @@ -187,12 +187,11 @@ module('Integration | Component | query-builder/conditions', function (hooks) { await render(TEMPLATE); const valueInput = find('.condition-content input[type="text"]'); - if (valueInput) { - await fillIn(valueInput, 'active'); - assert.strictEqual(changes[changes.length - 1].flat[0].value, 'active', 'the typed value is reported'); - } else { - assert.dom('.condition-group').exists('the seeded condition rendered'); - } + assert.ok(valueInput, 'the seeded condition renders a value input'); + + await fillIn(valueInput, 'active'); + + assert.strictEqual(changes[changes.length - 1].flat[0].value, 'active', 'the typed value is reported'); }); module('field and operator selection', function () { diff --git a/tests/integration/components/tab-navigation-test.js b/tests/integration/components/tab-navigation-test.js index e35ecc52..78da0945 100644 --- a/tests/integration/components/tab-navigation-test.js +++ b/tests/integration/components/tab-navigation-test.js @@ -168,7 +168,7 @@ module('Integration | Component | tab-navigation', function (hooks) { await render(TEMPLATE); const container = find('.tab-list').parentElement; - assert.true(container.className.includes('pills') || find('[role="tablist"]') !== null, 'the requested style is used'); + assert.dom('[role="tablist"]').exists('the requested style renders a tablist'); }); }); @@ -499,7 +499,7 @@ module('Integration | Component | tab-navigation', function (hooks) { }); // The style and size land on data-attributes, resolved by the component's own getters. - module('style and size', function () { + module('style and size arguments', function () { test('it defaults to the github style at medium size', async function (assert) { await render(TEMPLATE); diff --git a/tests/integration/components/table-test.js b/tests/integration/components/table-test.js index 630ac6d4..d1910866 100644 --- a/tests/integration/components/table-test.js +++ b/tests/integration/components/table-test.js @@ -734,17 +734,6 @@ module('Integration | Component | table imperative api', function (hooks) { assert.strictEqual(actions._stickyOffset, 150, 'and the one before it clears the default width'); }); - test('sticky cells are positioned in the dom', async function (assert) { - await render(TEMPLATE); - - const stickyHeader = find('thead th.is-sticky'); - if (stickyHeader) { - assert.strictEqual(stickyHeader.style.top, '0px', 'sticky headers are pinned vertically'); - } else { - assert.ok(true, 'no sticky header markup in this configuration'); - } - }); - test('scrolling the wrapper updates the shadow classes', async function (assert) { await render(TEMPLATE); diff --git a/tests/integration/components/table/cell/checkbox-test.js b/tests/integration/components/table/cell/checkbox-test.js index 8ec4c240..909afa39 100644 --- a/tests/integration/components/table/cell/checkbox-test.js +++ b/tests/integration/components/table/cell/checkbox-test.js @@ -33,7 +33,8 @@ module('Integration | Component | table/cell/checkbox', function (hooks) { await render(hbs``); const id = find('input[type="checkbox"]').getAttribute('id'); - assert.ok(id && id.length > 0, 'an id is still present'); + assert.ok(id, 'an id is still present'); + assert.true(id.length > 0, 'and it is not empty'); assert.notStrictEqual(id, 'undefined', 'the fallback is a real guid, not a stringified undefined'); }); diff --git a/tests/integration/components/table/cell/dropdown-test.js b/tests/integration/components/table/cell/dropdown-test.js index 559806d6..0d535514 100644 --- a/tests/integration/components/table/cell/dropdown-test.js +++ b/tests/integration/components/table/cell/dropdown-test.js @@ -210,7 +210,10 @@ module('Integration | Component | table/cell/dropdown', function (hooks) { assert.strictEqual(cell().style.zIndex, '1', 'the cell is raised while the menu is open'); await click(trigger()); - assert.strictEqual(cell().style.zIndex, before || '0', 'and lowered again on close'); + + // Normalised outside the assertion: an unset z-index reads back as ''. + const restored = before || '0'; + assert.strictEqual(cell().style.zIndex, restored, 'and lowered again on close'); }); }); }); diff --git a/tests/integration/components/template-builder/canvas-test.js b/tests/integration/components/template-builder/canvas-test.js index 3ade8b79..c9eb9a58 100644 --- a/tests/integration/components/template-builder/canvas-test.js +++ b/tests/integration/components/template-builder/canvas-test.js @@ -132,19 +132,14 @@ module('Integration | Component | template-builder/canvas', function (hooks) { `); - const child = find('[data-test-child]'); - if (child) { - await click(child); - assert.strictEqual(deselects, 0, 'only a direct background click clears the selection'); - } else { - // The canvas does not yield, so simulate the bubbling case directly. - const canvas = find('.tb-canvas'); - const inner = document.createElement('span'); - canvas.appendChild(inner); - await click(inner); - - assert.strictEqual(deselects, 0, 'only a direct background click clears the selection'); - } + // The canvas does not yield, so append a child directly to exercise the bubbling case. + const canvas = find('.tb-canvas'); + const inner = document.createElement('span'); + canvas.appendChild(inner); + + await click(inner); + + assert.strictEqual(deselects, 0, 'a click that merely bubbles through does not clear the selection'); }); test('clicking the background without a handler does not throw', async function (assert) { diff --git a/tests/integration/components/template-builder/element-renderer-test.js b/tests/integration/components/template-builder/element-renderer-test.js index 61b2666f..fc0d1cd3 100644 --- a/tests/integration/components/template-builder/element-renderer-test.js +++ b/tests/integration/components/template-builder/element-renderer-test.js @@ -210,7 +210,7 @@ module('Integration | Component | template-builder/element-renderer', function ( await render(TEMPLATE); const style = styleOf('.tb-element-text'); - assert.true(style.includes('border: 2px solid rgb(0, 0, 0)') || style.includes('border: 2px solid #000000'), 'default style and colour are used'); + assert.true(style.includes('border: 2px solid #000000'), `default style and colour are used (got ${style})`); assert.true(style.includes('border-radius: 4px')); }); diff --git a/tests/integration/components/template-builder/properties-panel-test.js b/tests/integration/components/template-builder/properties-panel-test.js index 8cd31c2b..66ccfcf6 100644 --- a/tests/integration/components/template-builder/properties-panel-test.js +++ b/tests/integration/components/template-builder/properties-panel-test.js @@ -289,13 +289,13 @@ module('Integration | Component | template-builder/properties-panel', function ( await render(TEMPLATE); - const numberInput = find('input[type="number"]'); - if (numberInput) { - await fillIn(numberInput, '300'); - assert.strictEqual(templateUpdates.length, 1, 'the template change is reported'); - } else { - assert.dom(this.element).exists('the canvas settings panel rendered'); - } + const nameInput = find('input[type="text"].tb-input'); + assert.ok(nameInput, 'the canvas settings panel offers a template name field'); + + await fillIn(nameInput, 'Invoice A4'); + + assert.strictEqual(templateUpdates.length, 1, 'the template change is reported'); + assert.strictEqual(templateUpdates[0].name, 'Invoice A4', 'with the new value'); }); }); @@ -308,11 +308,7 @@ module('Integration | Component | template-builder/properties-panel', function ( await render(TEMPLATE); const pickerButton = findAll('button').find((b) => /variable/i.test(b.textContent) || /\{\}/.test(b.textContent)); - if (!pickerButton) { - assert.strictEqual(opened.length, 0, 'no variable picker affordance for this element type'); - - return; - } + assert.ok(pickerButton, 'a text element offers a variable picker affordance'); await click(pickerButton); assert.strictEqual(opened.length, 1, 'the picker is opened for a target property'); @@ -618,7 +614,7 @@ module('Integration | Component | template-builder/properties-panel', function ( assert.dom('[data-test-panel="yes"]').exists(); }); - module('table columns', function () { + module('table column editing', function () { function columnInputs() { return findAll('input[placeholder="Column label"]'); } @@ -788,7 +784,7 @@ module('Integration | Component | template-builder/properties-panel', function ( }); }); - module('the image source', function () { + module('the image source picker', function () { function clearButton() { return findAll('button[title="Clear"]')[0]; } diff --git a/tests/integration/components/visible-column-picker-test.js b/tests/integration/components/visible-column-picker-test.js index 2a7cbb17..d25ae546 100644 --- a/tests/integration/components/visible-column-picker-test.js +++ b/tests/integration/components/visible-column-picker-test.js @@ -126,7 +126,7 @@ module('Integration | Component | visible-column-picker', function (hooks) { await click(TRIGGER); await click(checkboxes()[0]); - assert.false(this.columns[0].hidden === false, 'the column is now hidden'); + assert.notStrictEqual(this.columns[0].hidden, false, 'the column is now hidden'); assert.true(this.columns[0].hidden); assert.deepEqual(changes, [['name:hidden', 'status:hidden', 'internal_id:shown']]); }); diff --git a/tests/integration/helpers/unwrap-coordinates-test.js b/tests/integration/helpers/unwrap-coordinates-test.js index a43eec9b..9b744965 100644 --- a/tests/integration/helpers/unwrap-coordinates-test.js +++ b/tests/integration/helpers/unwrap-coordinates-test.js @@ -116,7 +116,8 @@ module('Integration | Helper | unwrap-coordinates', function (hooks) { await render(hbs`{{capture-value (unwrap-coordinates this.input)}}`); const coordinates = captured[0].coordinates; - assert.true(coordinates.lat < 85.06 && coordinates.lat > 85.04, `the latitude is clamped to the mercator maximum (got ${coordinates.lat})`); + assert.true(coordinates.lat < 85.06, `the latitude is clamped below the mercator maximum (got ${coordinates.lat})`); + assert.true(coordinates.lat > 85.04, `and not past it (got ${coordinates.lat})`); assert.true(Math.abs(coordinates.lng - 10) < TOLERANCE, 'the longitude is untouched'); }); diff --git a/tests/unit/services/modals-manager-test.js b/tests/unit/services/modals-manager-test.js index f45632f7..47b22182 100644 --- a/tests/unit/services/modals-manager-test.js +++ b/tests/unit/services/modals-manager-test.js @@ -51,7 +51,7 @@ module('Unit | Service | modals-manager', function (hooks) { assert.strictEqual(this.manager.options.title, 'Custom', 'the caller wins'); assert.strictEqual(this.manager.options.size, 'lg'); - assert.strictEqual(this.manager.options.backdrop, true, 'untouched defaults are preserved'); + assert.true(this.manager.options.backdrop, 'untouched defaults are preserved'); assert.strictEqual(this.manager.options.confirmButtonDefaultText, 'Yes'); }); @@ -374,7 +374,7 @@ module('Unit | Service | modals-manager', function (hooks) { assert.strictEqual(calls.length, 1); assert.strictEqual(calls[0], modal.options, 'the callback receives the modal options'); - assert.strictEqual(calls[0].backdrop, true, 'the options include the merged defaults'); + assert.true(calls[0].backdrop, 'the options include the merged defaults'); }); test('done invokes onFinish regardless of the action', async function (assert) { @@ -683,7 +683,7 @@ module('Unit | Service | modals-manager', function (hooks) { }); }); - module('userSelectOption', function () { + module('userSelectOption callbacks', function () { test('it offers the prompt with no options and resolves null when declined', async function (assert) { const selection = this.manager.userSelectOption('Pick a warehouse'); await settled(); diff --git a/tests/unit/utils/decorators/uses-transition-test.js b/tests/unit/utils/decorators/uses-transition-test.js index b1bec0e8..3d4680e6 100644 --- a/tests/unit/utils/decorators/uses-transition-test.js +++ b/tests/unit/utils/decorators/uses-transition-test.js @@ -60,7 +60,7 @@ module('Unit | Utility | decorators/uses-transition', function (hooks) { const value = this.build({ fade: 'truthy string' }).usesTransition; assert.strictEqual(typeof value, 'boolean'); - assert.strictEqual(value, true); + assert.true(value); }); test('transitions are disabled entirely under FastBoot', function (assert) { diff --git a/tests/unit/utils/get-active-url-param-test.js b/tests/unit/utils/get-active-url-param-test.js index b48b6ce8..49579132 100644 --- a/tests/unit/utils/get-active-url-param-test.js +++ b/tests/unit/utils/get-active-url-param-test.js @@ -17,18 +17,18 @@ module('Unit | Utility | get-active-url-param', function (hooks) { }); test('it returns boolean true', function (assert) { - assert.strictEqual(getActiveUrlParam(), true, 'the value is exactly true, not merely truthy'); + assert.true(getActiveUrlParam(), 'the value is exactly true, not merely truthy'); }); test('it ignores any arguments it is given', function (assert) { - assert.strictEqual(getActiveUrlParam('anything'), true); - assert.strictEqual(getActiveUrlParam(null, undefined, 0), true); + assert.true(getActiveUrlParam('anything')); + assert.true(getActiveUrlParam(null, undefined, 0)); }); test('it does not depend on the current query string', function (assert) { window.history.replaceState(null, '', '?active=false&other=1'); - assert.strictEqual(getActiveUrlParam(), true, 'the result is independent of the URL'); + assert.true(getActiveUrlParam(), 'the result is independent of the URL'); }); test('it is stable across repeated calls', function (assert) { diff --git a/tests/unit/utils/is-fast-boot-test.js b/tests/unit/utils/is-fast-boot-test.js index 73d837c9..5cb62017 100644 --- a/tests/unit/utils/is-fast-boot-test.js +++ b/tests/unit/utils/is-fast-boot-test.js @@ -15,7 +15,7 @@ module('Unit | Utility | is-fastboot', function (hooks) { setupTest(hooks); test('it returns false when no fastboot service is registered', function (assert) { - assert.strictEqual(isFastBoot(ownedContext(this.owner)), false, 'a browser app has no fastboot service'); + assert.false(isFastBoot(ownedContext(this.owner)), 'a browser app has no fastboot service'); }); test('it reports true when the fastboot service says so', function (assert) { diff --git a/tests/unit/utils/options-test.js b/tests/unit/utils/options-test.js index d3e8705b..7dd34200 100644 --- a/tests/unit/utils/options-test.js +++ b/tests/unit/utils/options-test.js @@ -10,7 +10,7 @@ module('Unit | Utility | options', function () { test('it returns the boolean true, not a truthy value', function (assert) { const result = options(); - assert.strictEqual(result, true); + assert.true(result); assert.strictEqual(typeof result, 'boolean'); }); @@ -24,6 +24,6 @@ module('Unit | Utility | options', function () { test('it returns the same value on repeated invocations', function (assert) { assert.strictEqual(options(), options(), 'the util is pure and stateless'); - assert.strictEqual(options.call({ scope: 'other' }), true, 'the receiver does not matter'); + assert.true(options.call({ scope: 'other' }), 'the receiver does not matter'); }); }); diff --git a/tests/unit/utils/permission-check-test.js b/tests/unit/utils/permission-check-test.js index d3c2d180..db15c12e 100644 --- a/tests/unit/utils/permission-check-test.js +++ b/tests/unit/utils/permission-check-test.js @@ -94,7 +94,7 @@ module('Unit | Utility | permission-check', function () { const result = checkPermission(abilities, 'fleet-ops view order', subject); - assert.strictEqual(result, true, 'the truthy result is coerced to true'); + assert.true(result, 'the truthy result is coerced to true'); assert.deepEqual(calls, [['fleet-ops view order', subject]], 'the permission string and subject are forwarded verbatim'); }); @@ -253,8 +253,8 @@ module('Unit | Utility | permission-check', function () { test('it coerces a non-boolean defaultWhenUnknown', function (assert) { const abilitiesService = abilitiesWithCan(() => true); - assert.strictEqual(evaluatePermission({ abilitiesService, defaultWhenUnknown: 'yes' }), true, 'truthy defaults coerce to true'); - assert.strictEqual(evaluatePermission({ abilitiesService, defaultWhenUnknown: 0 }), false, 'falsy defaults coerce to false'); + assert.true(evaluatePermission({ abilitiesService, defaultWhenUnknown: 'yes' }), 'truthy defaults coerce to true'); + assert.false(evaluatePermission({ abilitiesService, defaultWhenUnknown: 0 }), 'falsy defaults coerce to false'); }); test('it never calls the abilities service when resolution fails', function (assert) { diff --git a/tests/unit/utils/remove-nullish-test.js b/tests/unit/utils/remove-nullish-test.js index e2cfdfdc..ddd0bed6 100644 --- a/tests/unit/utils/remove-nullish-test.js +++ b/tests/unit/utils/remove-nullish-test.js @@ -15,7 +15,7 @@ module('Unit | Utility | remove-nullish', function () { assert.deepEqual(Object.keys(result).sort(), ['empty', 'nan', 'negZero', 'no', 'zero'], 'only null/undefined are nullish'); assert.strictEqual(result.zero, 0); assert.strictEqual(result.empty, ''); - assert.strictEqual(result.no, false); + assert.false(result.no); assert.true(Number.isNaN(result.nan)); }); diff --git a/tests/unit/utils/report-builder-test.js b/tests/unit/utils/report-builder-test.js index 4dbf2e15..6d760f1c 100644 --- a/tests/unit/utils/report-builder-test.js +++ b/tests/unit/utils/report-builder-test.js @@ -115,7 +115,7 @@ module('Unit | Utility | report-builder', function () { test('it round-trips primitives and arrays', function (assert) { assert.strictEqual(deepClone(0), 0, 'zero is not treated as nullish'); assert.strictEqual(deepClone(''), '', 'an empty string is not treated as nullish'); - assert.strictEqual(deepClone(false), false); + assert.false(deepClone(false)); assert.strictEqual(deepClone('text'), 'text'); assert.deepEqual(deepClone([1, [2]]), [1, [2]]); assert.true(Array.isArray(deepClone([])), 'array-ness survives the round trip'); @@ -204,8 +204,8 @@ module('Unit | Utility | report-builder', function () { module('default export', function () { test('it is a function returning boolean true', function (assert) { assert.strictEqual(typeof reportBuilder, 'function'); - assert.strictEqual(reportBuilder(), true); - assert.strictEqual(reportBuilder('ignored', 1), true, 'arguments are ignored'); + assert.true(reportBuilder()); + assert.true(reportBuilder('ignored', 1), 'arguments are ignored'); }); }); });