From b89a946d8d7a68185b896bb600e6a14cd2399e21 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 20:56:21 +0800 Subject: [PATCH 01/13] test: make the engine's own test suite bootable The dummy app never ran: ember-classic-decorator (unused) crashes on Ember 5, lazy loading kept the engine's modules out of the test bundle, and the host services, config and intl data the panels need were missing. Mirrors what fleetops already does: eager loading when `ember test` runs from this package, an inert stand-in for the console's extensions module, the API config the core adapter reads at module scope, and the legacy built-in components the engine's templates still use. The dummy intl service ignores missing locale data, since headless Chrome has no number formats for every bundled locale, and the order details and network controller tests stub the host router, fetch and app cache the way the widget tests already do. --- index.js | 8 +- package.json | 4 +- pnpm-lock.yaml | 84 +++++++++++-------- tests/dummy/app/services/intl.js | 18 ++++ tests/dummy/config/environment.js | 14 ++++ .../components/shared-identity-test.js | 4 +- .../storefront/order/details-test.js | 53 ++++++++++-- .../components/widget/customers-test.js | 8 ++ tests/test-helper.js | 11 +++ .../unit/controllers/customers/index-test.js | 2 + .../networks/index/network/customers-test.js | 8 ++ .../networks/index/network/orders-test.js | 8 ++ tests/unit/controllers/orders/index-test.js | 2 + 13 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 tests/dummy/app/services/intl.js diff --git a/index.js b/index.js index 005d7714..10a66128 100644 --- a/index.js +++ b/index.js @@ -8,8 +8,14 @@ const path = require('path'); module.exports = buildEngine({ name, + // Lazy loading keeps the engine out of the host bundle, but it also keeps the + // engine's modules out of the dummy app that `ember test` builds, so every test + // importing `@fleetbase/storefront-engine/*` fails to load. Eager loading is + // scoped to `ember test` run from this package, so host apps consuming the + // engine, including their own test builds, keep lazy loading. Addon index + // files are evaluated before ember-cli assigns EMBER_ENV, hence the argv check. lazyLoading: { - enabled: true, + enabled: !(process.argv.includes('test') && process.cwd() === __dirname), }, isDevelopingAddon() { diff --git a/package.json b/package.json index 344a57db..33417fed 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@babel/core": "^7.23.2", "@fleetbase/ember-core": "^0.3.25", "@fleetbase/ember-ui": "^0.4.5", - "@fleetbase/fleetops-data": "^0.2.3", + "@fleetbase/fleetops-data": "^0.2.2", "@fortawesome/ember-fontawesome": "^2.0.0", "@fortawesome/fontawesome-svg-core": "6.4.0", "@fortawesome/free-brands-svg-icons": "6.4.0", @@ -61,6 +61,7 @@ "devDependencies": { "@babel/eslint-parser": "^7.22.15", "@babel/plugin-proposal-decorators": "^7.23.2", + "@ember/legacy-built-in-components": "^0.4.2", "@ember/optional-features": "^2.0.0", "@ember/test-helpers": "^3.2.0", "@embroider/test-setup": "^3.0.2", @@ -71,7 +72,6 @@ "broccoli-funnel": "^3.0.8", "broccoli-merge-trees": "^4.2.0", "concurrently": "^8.2.2", - "ember-classic-decorator": "^3.0.0", "ember-cli": "~5.4.1", "ember-cli-clean-css": "^3.0.0", "ember-cli-dependency-checker": "^3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 676fd872..091b4870 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^0.4.5 version: 0.4.5(fd4e3593913b6af1971bbb09f229d016) '@fleetbase/fleetops-data': - specifier: ^0.1.37 - version: 0.1.37(d08c013ce78c1181b04d076f9e3ef4de) + specifier: ^0.2.2 + version: 0.2.2(d08c013ce78c1181b04d076f9e3ef4de) '@fortawesome/ember-fontawesome': specifier: ^2.0.0 version: 2.0.0(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(rollup@2.80.0)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)) @@ -57,6 +57,9 @@ importers: '@babel/plugin-proposal-decorators': specifier: ^7.23.2 version: 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@ember/legacy-built-in-components': + specifier: ^0.4.2 + version: 0.4.2(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1) '@ember/optional-features': specifier: ^2.0.0 version: 2.3.0(supports-color@8.1.1) @@ -87,9 +90,6 @@ importers: concurrently: specifier: ^8.2.2 version: 8.2.2 - ember-classic-decorator: - specifier: ^3.0.0 - version: 3.0.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) ember-cli: specifier: ~5.4.1 version: 5.4.2(@babel/core@7.29.7(supports-color@8.1.1))(@types/node@25.9.1)(debug@4.4.3(supports-color@8.1.1))(handlebars@4.7.9)(supports-color@8.1.1)(underscore@1.13.8) @@ -116,7 +116,7 @@ importers: version: 4.12.8(@babel/core@7.29.7(supports-color@8.1.1))(@ember/string@3.1.1(supports-color@8.1.1))(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)) ember-engines: specifier: ^0.9.0 - version: 0.9.0(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1) + version: 0.9.0(@babel/core@7.29.7(supports-color@8.1.1))(@ember/legacy-built-in-components@0.4.2(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1) ember-load-initializers: specifier: ^2.1.2 version: 2.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) @@ -1147,6 +1147,12 @@ packages: '@ember/edition-utils@1.2.0': resolution: {integrity: sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==} + '@ember/legacy-built-in-components@0.4.2': + resolution: {integrity: sha512-rJulbyVQIVe1zEDQDqAQHechHy44DsS2qxO24+NmU/AYxwPFSzWC/OZNCDFSfLU+Y5BVd/00qjxF0pu7Nk+TNA==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + ember-source: '*' + '@ember/optional-features@2.3.0': resolution: {integrity: sha512-+M8CkPledQEaDbfIlwlq6Phgpm5jdT3a6WVDJk7b/zadw5xAJkuQKVK7DgR0SFgHGiWlyn6a8AU5p2mCA706RA==} engines: {node: 10.* || 12.* || >= 14} @@ -1283,14 +1289,14 @@ packages: resolution: {integrity: sha512-C/+OvMpH5fCKYY5v+CteR6Tx6IAI33nfvOrmo7mFGDnyKDsjplsVVgWpsecLG94fJvpuVVI+CFvaiH50GUM3Mg==} engines: {node: '>= 18'} - '@fleetbase/fleetops-data@0.1.37': - resolution: {integrity: sha512-VGVWaGlwQAqHWA9/A80jKAjoDkAM7U9Ve1UVmKkCTlolqUd5/k/Z66rVC0iFUijj8qzkMCdahdh3mBPQ6x71WA==} - engines: {node: '>= 18'} - '@fleetbase/fleetops-data@0.1.40': resolution: {integrity: sha512-bp4tWaS2cpnitjtmH9hm3T7milwSqgt4j3T/KA8cvsFpbg7p303ADb5CNml5RsribgdBcRYtMsLEFYeYnU9dpg==} engines: {node: '>= 18'} + '@fleetbase/fleetops-data@0.2.2': + resolution: {integrity: sha512-HDgcGpJAk8q+sSmV070nCh02o4S0kuFCvwrf+Ltb+IrUkQ2RhDL/w8gdrqM65HrgbgWxQnslIeqzKfF6pV4/DA==} + engines: {node: '>= 18'} + '@fleetbase/intl-lint@0.0.1': resolution: {integrity: sha512-LkjxJr15hSiGmqh3JwixcpjmkhXNieNAEgQUVv1Duo50jTr/D5WXEyOaeI8wuVnVFhT+FS/DMqm403DgSKlsEg==} hasBin: true @@ -3140,6 +3146,7 @@ packages: concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} concurrently@8.2.2: resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} @@ -3750,10 +3757,6 @@ packages: ember-resolver: '>= 8.0.0' ember-source: ^3.28.0 || ^4.0.0 || >=5.0.0 - ember-classic-decorator@3.0.1: - resolution: {integrity: sha512-IoocHPX1cY93Sma7VeDGbF0M1+TkBWDJLus+RD01902eD+KHEL2+diwM+iayCyO+1/xaUzdzNGh35J+i+TCxoA==} - engines: {node: 12.* || 14.* || >= 16} - ember-cli-babel-plugin-helpers@1.1.1: resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} engines: {node: 6.* || 8.* || >= 10.*} @@ -9866,6 +9869,18 @@ snapshots: '@ember/edition-utils@1.2.0': {} + '@ember/legacy-built-in-components@0.4.2(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1)': + dependencies: + '@embroider/macros': 1.20.3(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + ember-cli-babel: 7.26.11(supports-color@8.1.1) + ember-cli-htmlbars: 5.7.2(supports-color@8.1.1) + ember-cli-typescript: 4.2.1(supports-color@8.1.1) + ember-source: 5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + '@ember/optional-features@2.3.0(supports-color@8.1.1)': dependencies: chalk: 4.1.2 @@ -9953,7 +9968,7 @@ snapshots: find-up: 5.0.0 lodash: 4.18.1 resolve: 1.22.12 - semver: 7.8.1 + semver: 7.8.5 transitivePeerDependencies: - '@babel/core' - supports-color @@ -10001,10 +10016,10 @@ snapshots: js-string-escape: 1.0.1 lodash: 4.18.1 minimatch: 3.1.5 - pkg-entry-points: 1.1.1 + pkg-entry-points: 1.1.2 resolve-package-path: 4.0.3 resolve.exports: 2.0.3 - semver: 7.8.1 + semver: 7.8.5 typescript-memoize: 1.1.1 transitivePeerDependencies: - supports-color @@ -10249,6 +10264,7 @@ snapshots: - ember-cli-mirage - ember-resolver - ember-source + - encoding - eslint - miragejs - postcss @@ -10263,7 +10279,7 @@ snapshots: - webpack-command - yaml - '@fleetbase/fleetops-data@0.1.37(d08c013ce78c1181b04d076f9e3ef4de)': + '@fleetbase/fleetops-data@0.1.40(d08c013ce78c1181b04d076f9e3ef4de)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) '@fleetbase/ember-core': 0.3.25(d08c013ce78c1181b04d076f9e3ef4de) @@ -10284,10 +10300,10 @@ snapshots: - utf-8-validate - webpack - '@fleetbase/fleetops-data@0.1.40(d08c013ce78c1181b04d076f9e3ef4de)': + '@fleetbase/fleetops-data@0.2.2(d08c013ce78c1181b04d076f9e3ef4de)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) - '@fleetbase/ember-core': 0.3.24(d08c013ce78c1181b04d076f9e3ef4de) + '@fleetbase/ember-core': 0.3.25(d08c013ce78c1181b04d076f9e3ef4de) date-fns: 2.30.0 ember-cli-babel: 8.3.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) ember-cli-htmlbars: 6.3.0(supports-color@8.1.1) @@ -10298,6 +10314,7 @@ snapshots: - bufferutil - ember-resolver - ember-source + - encoding - eslint - supports-color - typescript @@ -11055,7 +11072,7 @@ snapshots: '@types/glob@9.0.0': dependencies: - glob: 8.1.0 + glob: 13.0.6 '@types/http-errors@2.0.5': {} @@ -11080,7 +11097,7 @@ snapshots: '@types/minimatch@6.0.0': dependencies: - minimatch: 7.4.9 + minimatch: 10.2.5 '@types/minimist@1.2.5': {} @@ -11586,7 +11603,7 @@ snapshots: babel-generator: 6.26.1 babel-helpers: 6.24.1(supports-color@8.1.1) babel-messages: 6.23.0 - babel-register: 6.26.0 + babel-register: 6.26.0(supports-color@8.1.1) babel-runtime: 6.26.0 babel-template: 6.26.0(supports-color@8.1.1) babel-traverse: 6.26.0(supports-color@8.1.1) @@ -11742,7 +11759,7 @@ snapshots: babel-plugin-syntax-dynamic-import@6.18.0: {} - babel-register@6.26.0: + babel-register@6.26.0(supports-color@8.1.1): dependencies: babel-core: 6.26.3(supports-color@8.1.1) babel-runtime: 6.26.0 @@ -11751,6 +11768,8 @@ snapshots: lodash: 4.18.1 mkdirp: 0.5.6 source-map-support: 0.4.18 + transitivePeerDependencies: + - supports-color babel-runtime@6.26.0: dependencies: @@ -13512,17 +13531,6 @@ snapshots: - '@babel/core' - supports-color - ember-classic-decorator@3.0.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): - dependencies: - '@embroider/macros': 1.20.3(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) - babel-plugin-filter-imports: 4.0.0 - ember-cli-babel: 7.26.11(supports-color@8.1.1) - ember-cli-htmlbars: 6.3.0(supports-color@8.1.1) - transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - supports-color - ember-cli-babel-plugin-helpers@1.1.1: {} ember-cli-babel@7.26.11(supports-color@8.1.1): @@ -13655,7 +13663,7 @@ snapshots: hash-for-dep: 1.5.2(supports-color@8.1.1) heimdalljs-logger: 0.1.10(supports-color@8.1.1) json-stable-stringify: 1.3.0 - semver: 7.8.1 + semver: 7.8.5 silent-error: 1.1.1(supports-color@8.1.1) strip-bom: 4.0.0 walk-sync: 2.2.0 @@ -14173,7 +14181,7 @@ snapshots: transitivePeerDependencies: - supports-color - ember-engines@0.9.0(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1): + ember-engines@0.9.0(@babel/core@7.29.7(supports-color@8.1.1))(@ember/legacy-built-in-components@0.4.2(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1): dependencies: '@embroider/macros': 1.20.3(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) amd-name-resolver: 1.3.1 @@ -14193,6 +14201,8 @@ snapshots: ember-cli-version-checker: 5.1.2(supports-color@8.1.1) ember-source: 5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)) lodash: 4.18.1 + optionalDependencies: + '@ember/legacy-built-in-components': 0.4.2(@babel/core@7.29.7(supports-color@8.1.1))(ember-source@5.4.1(@babel/core@7.29.7(supports-color@8.1.1))(@glimmer/component@1.1.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(rsvp@4.8.5)(supports-color@8.1.1)(webpack@5.107.2(clean-css@5.3.3)(postcss@8.5.15)(uglify-js@3.19.3)))(supports-color@8.1.1) transitivePeerDependencies: - '@babel/core' - '@glint/template' diff --git a/tests/dummy/app/services/intl.js b/tests/dummy/app/services/intl.js new file mode 100644 index 00000000..90cd3308 --- /dev/null +++ b/tests/dummy/app/services/intl.js @@ -0,0 +1,18 @@ +import IntlService from 'ember-intl/services/intl'; + +/** + * ember-intl builds a formatter for every bundled locale when the service is + * created. Headless Chrome ships no number-format data for some of them + * (Mongolian among them), which ember-intl's default handler turns into a thrown + * error inside whichever test first touches intl. Missing locale data and + * missing translations are not what any test here checks, so both are ignored. + */ +export default class DummyIntlService extends IntlService { + onIntlError(error) { + if (error?.code === 'MISSING_DATA' || error?.code === 'MISSING_TRANSLATION') { + return; + } + + super.onIntlError(error); + } +} diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index 61f3a09d..12eb0fdb 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -14,6 +14,20 @@ module.exports = function (environment) { }, }, + // `@fleetbase/ember-core`'s application adapter reads `config.API.host` at + + // module scope, so the dummy application must declare it before any + + // adapter module can even be evaluated. The values are deliberately + + // obviously-fake: nothing in the suite performs a real request. + + API: { + host: 'https://api.fleetbase.test', + + namespace: 'int/v1', + }, + APP: { // Here you can pass flags/options to your application instance // when it is created diff --git a/tests/integration/components/shared-identity-test.js b/tests/integration/components/shared-identity-test.js index 00bf135c..aed5eba7 100644 --- a/tests/integration/components/shared-identity-test.js +++ b/tests/integration/components/shared-identity-test.js @@ -17,7 +17,7 @@ module('Integration | Component | shared identity components', function (hooks) }); test('a driver identity cell renders the name, status dot and vehicle badge from the shared descriptor', async function (assert) { - this.set('row', { driver_assigned: { resourceType: 'driver', name: 'Ada Driver', status: 'available', vehicle_name: 'Truck 10' } }); + this.set('row', { driver_assigned: { resourceType: 'driver', name: 'Ada Driver', status: 'available', online: true, vehicle_name: 'Truck 10' } }); this.set('column', { resourceType: 'driver', resourcePath: 'driver_assigned', popover: false }); await render(hbs``); @@ -52,7 +52,7 @@ module('Integration | Component | shared identity components', function (hooks) `); assert.dom('[data-test-customer] [data-test-resource-pill-title]').hasText('Ava Chen'); - assert.dom('[data-test-customer] [data-test-resource-pill-subtitle]').hasText('Customer'); + assert.dom('[data-test-customer] [data-test-resource-pill-subtitle]').hasText('Contact', 'the polymorphic type reads as a person would say it'); assert.dom('[data-test-driver] [data-test-resource-pill-title]').hasText('Ada Driver'); assert.dom('[data-test-driver] [data-test-resource-pill-subtitle]').hasText('+15550100'); assert.dom('[data-test-place] [data-test-resource-pill-title]').hasText('Depot'); diff --git a/tests/integration/components/storefront/order/details-test.js b/tests/integration/components/storefront/order/details-test.js index 981b8944..185ebc4a 100644 --- a/tests/integration/components/storefront/order/details-test.js +++ b/tests/integration/components/storefront/order/details-test.js @@ -1,13 +1,49 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'dummy/tests/helpers'; -import { render } from '@ember/test-helpers'; +import { setupIntl } from 'ember-intl/test-support'; +import { render, findAll } from '@ember/test-helpers'; +import Service from '@ember/service'; import { hbs } from 'ember-cli-htmlbars'; import { initialize } from '@fleetbase/fleetops-data/instance-initializers/register-shared-resource-descriptors'; module('Integration | Component | storefront/order/details', function (hooks) { setupRenderingTest(hooks); + setupIntl(hooks, 'en-us'); hooks.beforeEach(function () { + // The activity, route and tracking panels fetch on insert and the dummy app has + // no API. A request that never answers leaves each task pending, and ember-concurrency + // cancels it with its component, so nothing writes to a torn-down panel. + this.owner.register( + 'service:fetch', + class extends Service { + get() { + return new Promise(() => {}); + } + + post() { + return new Promise(() => {}); + } + } + ); + // The app cache keeps a proxy that outlives a test's owner; the activity panel reads + // it while initialising, so give it an inert cache like the widget tests do. + this.owner.register( + 'service:app-cache', + class extends Service { + get() { + return undefined; + } + + set() {} + + setEmberData() {} + + getEmberData() { + return []; + } + } + ); initialize(this.owner); }); @@ -55,13 +91,14 @@ module('Integration | Component | storefront/order/details', function (hooks) { await render(hbs``); - assert.dom('.next-content-panel-title-container').hasTextContaining('Activity'); - assert.dom('.next-content-panel-title-container').hasTextContaining('Store'); - assert.dom('.next-content-panel-title-container').hasTextContaining('Order'); - assert.dom('.next-content-panel-title-container').hasTextContaining('Details'); - assert.dom('.next-content-panel-title-container').hasTextContaining('Customer Insights'); - assert.dom('.next-content-panel-title-container').hasTextContaining('Route'); - assert.dom('.next-content-panel-title-container').hasTextContaining('Metadata'); + const titles = findAll('.next-content-panel-title-container').map((element) => element.textContent.trim()); + + for (const title of ['Activity', 'Store', 'Order', 'Details', 'Customer Insights', 'Route', 'Metadata']) { + assert.ok( + titles.some((text) => text.includes(title)), + `a panel titled ${title} renders` + ); + } assert.dom('.storefront-order-person__name').hasTextContaining('Tasty Store'); assert.dom('[data-test-order-driver-pill] [data-test-resource-pill-title]').hasText('No driver assigned', 'an unassigned driver is a static pill with the fallback title'); assert.dom('[data-test-order-customer-pill] [data-test-resource-pill-title]').hasText('No customer'); diff --git a/tests/integration/components/widget/customers-test.js b/tests/integration/components/widget/customers-test.js index 4aa03639..9a9f20b2 100644 --- a/tests/integration/components/widget/customers-test.js +++ b/tests/integration/components/widget/customers-test.js @@ -26,6 +26,14 @@ class FetchStubService extends Service { } class IntlStubService extends Service { + exists() { + return false; + } + + onLocaleChanged() { + return () => {}; + } + t(key) { if (key === 'storefront.component.widget.customers.widget-title') { return 'Recent Customers'; diff --git a/tests/test-helper.js b/tests/test-helper.js index 4efd6e58..d355f8d0 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -5,6 +5,17 @@ import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; import { start } from 'ember-qunit'; +// The universe extension manager imports a module the console host generates at build +// time. The engine's dummy app has no host, so provide an inert stand-in before boot. +if (typeof window.define === 'function' && !window.requirejs?.entries?.['@fleetbase/console/extensions']) { + window.define('@fleetbase/console/extensions', ['exports'], function (exports) { + exports.getExtensionLoader = function () { + return null; + }; + exports.default = {}; + }); +} + setApplication(Application.create(config.APP)); setup(QUnit.assert); diff --git a/tests/unit/controllers/customers/index-test.js b/tests/unit/controllers/customers/index-test.js index d8189724..d29291a8 100644 --- a/tests/unit/controllers/customers/index-test.js +++ b/tests/unit/controllers/customers/index-test.js @@ -1,8 +1,10 @@ import { module, test } from 'qunit'; import { setupTest } from 'dummy/tests/helpers'; +import { setupIntl } from 'ember-intl/test-support'; module('Unit | Controller | customers/index', function (hooks) { setupTest(hooks); + setupIntl(hooks, 'en-us'); test('it exists', function (assert) { let controller = this.owner.lookup('controller:customers/index'); diff --git a/tests/unit/controllers/networks/index/network/customers-test.js b/tests/unit/controllers/networks/index/network/customers-test.js index bd90c9fb..b86fd691 100644 --- a/tests/unit/controllers/networks/index/network/customers-test.js +++ b/tests/unit/controllers/networks/index/network/customers-test.js @@ -1,8 +1,16 @@ import { module, test } from 'qunit'; import { setupTest } from 'dummy/tests/helpers'; +import { setupIntl } from 'ember-intl/test-support'; +import Service from '@ember/service'; module('Unit | Controller | networks/index/network/customers', function (hooks) { setupTest(hooks); + setupIntl(hooks, 'en-us'); + + hooks.beforeEach(function () { + // The engine's dummy app has no console host to provide the host router. + this.owner.register('service:hostRouter', class extends Service {}); + }); test('it exists', function (assert) { let controller = this.owner.lookup('controller:networks/index/network/customers'); diff --git a/tests/unit/controllers/networks/index/network/orders-test.js b/tests/unit/controllers/networks/index/network/orders-test.js index 141a91cc..a6b26e5d 100644 --- a/tests/unit/controllers/networks/index/network/orders-test.js +++ b/tests/unit/controllers/networks/index/network/orders-test.js @@ -1,8 +1,16 @@ import { module, test } from 'qunit'; import { setupTest } from 'dummy/tests/helpers'; +import { setupIntl } from 'ember-intl/test-support'; +import Service from '@ember/service'; module('Unit | Controller | networks/index/network/orders', function (hooks) { setupTest(hooks); + setupIntl(hooks, 'en-us'); + + hooks.beforeEach(function () { + // The engine's dummy app has no console host to provide the host router. + this.owner.register('service:hostRouter', class extends Service {}); + }); test('it exists', function (assert) { let controller = this.owner.lookup('controller:networks/index/network/orders'); diff --git a/tests/unit/controllers/orders/index-test.js b/tests/unit/controllers/orders/index-test.js index 6c9ca5d4..5a2748a1 100644 --- a/tests/unit/controllers/orders/index-test.js +++ b/tests/unit/controllers/orders/index-test.js @@ -1,9 +1,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'dummy/tests/helpers'; +import { setupIntl } from 'ember-intl/test-support'; import Service from '@ember/service'; module('Unit | Controller | orders/index', function (hooks) { setupTest(hooks); + setupIntl(hooks, 'en-us'); test('it exists', function (assert) { let controller = this.owner.lookup('controller:orders/index'); From 3c6ad104cdd805ad89dd676b382b540a1df689bb Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sat, 10 Oct 2026 12:03:38 +0800 Subject: [PATCH 02/13] feat(food-trucks): networks show their member stores' trucks; a truck is only found by its own storefront --- .../Controllers/v1/FoodTruckController.php | 56 ++++++++++++++----- server/src/Http/Resources/FoodTruck.php | 8 +++ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/server/src/Http/Controllers/v1/FoodTruckController.php b/server/src/Http/Controllers/v1/FoodTruckController.php index be35a803..4f07070b 100644 --- a/server/src/Http/Controllers/v1/FoodTruckController.php +++ b/server/src/Http/Controllers/v1/FoodTruckController.php @@ -5,6 +5,7 @@ use Fleetbase\Http\Controllers\Controller; use Fleetbase\Storefront\Http\Resources\FoodTruck as FoodTruckResource; use Fleetbase\Storefront\Models\FoodTruck; +use Fleetbase\Storefront\Models\Network; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; @@ -17,25 +18,47 @@ class FoodTruckController extends Controller */ public function query(Request $request) { - $limit = $request->input('limit', false); - $offset = $request->input('offset', false); - $results = []; + $limit = $request->input('limit', false); + $offset = $request->input('offset', false); + $storeIds = static::storeUuidsForStorefront(); + if (empty($storeIds)) { + return FoodTruckResource::collection([]); + } + + $results = FoodTruck::queryWithRequestCached($request, function (&$query) use ($limit, $offset, $storeIds) { + $query->whereIn('store_uuid', $storeIds)->with(['store', 'vehicle', 'zone', 'serviceArea', 'catalogs']); + + if ($limit) { + $query->limit($limit); + } + + if ($offset) { + $query->offset($offset); + } + }); + return FoodTruckResource::collection($results); + } + + /** + * The stores whose food trucks this storefront shows: a store's own, or in a network + * every member store's, so a network can run trucks alongside its stores. + * + * @return array + */ + protected static function storeUuidsForStorefront(): array + { if (session('storefront_store')) { - $results = FoodTruck::queryWithRequestCached($request, function (&$query) use ($limit, $offset) { - $query->where('store_uuid', session('storefront_store')); + return [session('storefront_store')]; + } - if ($limit) { - $query->limit($limit); - } + if (session('storefront_network')) { + $network = Network::where('uuid', session('storefront_network'))->first(); - if ($offset) { - $query->offset($offset); - } - }); + return $network ? $network->stores()->pluck('stores.uuid')->all() : []; } - return FoodTruckResource::collection($results); + return []; } /** @@ -52,6 +75,13 @@ public function find($id) return response()->error('Food Truck resource not found.'); } + // Only a truck of this storefront (its own, or a network member store's). + if (!in_array($foodTruck->store_uuid, static::storeUuidsForStorefront(), true)) { + return response()->error('Food Truck resource not found.', 404); + } + + $foodTruck->loadMissing(['store', 'vehicle', 'zone', 'serviceArea', 'catalogs']); + // response the product resource return new FoodTruckResource($foodTruck); } diff --git a/server/src/Http/Resources/FoodTruck.php b/server/src/Http/Resources/FoodTruck.php index 942e3624..028be3e6 100644 --- a/server/src/Http/Resources/FoodTruck.php +++ b/server/src/Http/Resources/FoodTruck.php @@ -36,6 +36,14 @@ public function toArray($request) 'location' => $this->vehicle ? $this->vehicle->location : null, 'online' => $this->vehicle ? $this->vehicle->online : false, 'status' => $this->status, + // The store the truck belongs to, so a network can name it and order from it. + 'store' => $this->whenLoaded('store', fn () => $this->store ? [ + 'id' => $this->store->public_id, + 'name' => $this->store->name, + 'logo_url' => $this->store->logo_url, + 'currency' => $this->store->currency, + 'options' => $this->store->options, + ] : null), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; From e60a7066048f28aa5f12a8c3d08a6511854afb81 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sat, 10 Oct 2026 12:30:24 +0800 Subject: [PATCH 03/13] feat(cards): catalogs, trucks and networks render on the shared resource card Catalog, truck and network index pages use Layout::Resource::CardsGrid with one card each on Layout::Resource::Card, following the Fleet-Ops anatomy: identity and subtitle in the header, media or facts in the body, status, icon actions and meta in the footer, with registry slots at each end. A catalog card shows its categories with product counts, its hours as a seven-day strip (the new Storefront::HoursStrip) and its product total; a draft is dashed. A truck card is a Vehicle::Pill with the online dot, the vehicle photo, where it serves, and a presence badge that says serving, offline, inactive, or online with no catalog. A network card shows the logo on its backdrop, the online state, the currency and member count. "Food trucks" reads "Trucks" in the UI; the food-truck model, routes and permissions keep their names. The catalogs and trucks pages gain the search the header already offered. --- addon/components/modals/create-food-truck.hbs | 12 +- addon/components/storefront/catalog/card.hbs | 53 +++++ addon/components/storefront/catalog/card.js | 48 ++++ addon/components/storefront/hours-strip.hbs | 8 + addon/components/storefront/hours-strip.js | 34 +++ addon/components/storefront/network/card.hbs | 44 ++++ addon/components/storefront/network/card.js | 29 +++ addon/components/storefront/truck/card.hbs | 49 ++++ addon/components/storefront/truck/card.js | 74 ++++++ addon/controllers/application.js | 2 +- addon/controllers/catalogs/index.js | 17 +- addon/controllers/food-trucks/index.js | 27 ++- addon/models/food-truck.js | 2 + addon/styles/storefront-engine.css | 225 ++++++++++++++++++ addon/templates/catalogs/index.hbs | 57 ++--- addon/templates/food-trucks/index.hbs | 59 ++--- addon/templates/networks/index.hbs | 91 ++----- app/components/storefront/catalog/card.js | 1 + app/components/storefront/hours-strip.js | 1 + app/components/storefront/network/card.js | 1 + app/components/storefront/truck/card.js | 1 + .../storefront/catalog/card-test.js | 53 +++++ .../components/storefront/hours-strip-test.js | 33 +++ .../storefront/network/card-test.js | 48 ++++ .../components/storefront/truck/card-test.js | 59 +++++ translations/en-us.yaml | 32 ++- 26 files changed, 888 insertions(+), 172 deletions(-) create mode 100644 addon/components/storefront/catalog/card.hbs create mode 100644 addon/components/storefront/catalog/card.js create mode 100644 addon/components/storefront/hours-strip.hbs create mode 100644 addon/components/storefront/hours-strip.js create mode 100644 addon/components/storefront/network/card.hbs create mode 100644 addon/components/storefront/network/card.js create mode 100644 addon/components/storefront/truck/card.hbs create mode 100644 addon/components/storefront/truck/card.js create mode 100644 app/components/storefront/catalog/card.js create mode 100644 app/components/storefront/hours-strip.js create mode 100644 app/components/storefront/network/card.js create mode 100644 app/components/storefront/truck/card.js create mode 100644 tests/integration/components/storefront/catalog/card-test.js create mode 100644 tests/integration/components/storefront/hours-strip-test.js create mode 100644 tests/integration/components/storefront/network/card-test.js create mode 100644 tests/integration/components/storefront/truck/card-test.js diff --git a/addon/components/modals/create-food-truck.hbs b/addon/components/modals/create-food-truck.hbs index 3332b6b6..cfc3e543 100644 --- a/addon/components/modals/create-food-truck.hbs +++ b/addon/components/modals/create-food-truck.hbs @@ -1,11 +1,11 @@