diff --git a/index.js b/index.js index 8e4b98eb5..b2411839c 100644 --- a/index.js +++ b/index.js @@ -9,8 +9,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/fleetops-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), }, treeForLeaflet: function () { diff --git a/package.json b/package.json index 51089688d..3edcd1006 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1856586e2..e530277b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: '@babel/plugin-proposal-decorators': specifier: ^7.23.2 version: 7.29.0(@babel/core@7.29.0) + '@ember/legacy-built-in-components': + specifier: ^0.4.2 + version: 0.4.2(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))) '@ember/optional-features': specifier: ^2.0.0 version: 2.3.0 @@ -161,7 +164,7 @@ importers: version: 4.12.8(@babel/core@7.29.0)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14)) ember-engines: specifier: ^0.9.0 - version: 0.9.0(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))) + version: 0.9.0(@babel/core@7.29.0)(@ember/legacy-built-in-components@0.4.2(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))))(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))) ember-load-initializers: specifier: ^2.1.2 version: 2.1.2(@babel/core@7.29.0) @@ -1253,6 +1256,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} @@ -10103,6 +10112,18 @@ snapshots: '@ember/edition-utils@1.2.0': {} + '@ember/legacy-built-in-components@0.4.2(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14)))': + dependencies: + '@embroider/macros': 1.20.2(@babel/core@7.29.0) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + ember-cli-typescript: 4.2.1 + ember-source: 5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14)) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + '@ember/optional-features@2.3.0': dependencies: chalk: 4.1.2 @@ -14455,7 +14476,7 @@ snapshots: transitivePeerDependencies: - supports-color - ember-engines@0.9.0(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))): + ember-engines@0.9.0(@babel/core@7.29.0)(@ember/legacy-built-in-components@0.4.2(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))))(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))): dependencies: '@embroider/macros': 1.20.2(@babel/core@7.29.0) amd-name-resolver: 1.3.1 @@ -14475,6 +14496,8 @@ snapshots: ember-cli-version-checker: 5.1.2 ember-source: 5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14)) lodash: 4.18.1 + optionalDependencies: + '@ember/legacy-built-in-components': 0.4.2(@babel/core@7.29.0)(ember-source@5.4.1(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.106.2(postcss@8.5.14))) transitivePeerDependencies: - '@babel/core' - '@glint/template' diff --git a/server/tests/CustomerContactIdentitySafetyTest.php b/server/tests/CustomerContactIdentitySafetyTest.php index 0b56c8136..b2d6ecf68 100644 --- a/server/tests/CustomerContactIdentitySafetyTest.php +++ b/server/tests/CustomerContactIdentitySafetyTest.php @@ -32,12 +32,10 @@ $apiContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/ContactController.php'); $internalContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/ContactController.php'); $orderController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/OrderController.php'); - $customerForm = file_get_contents(__DIR__ . '/../../addon/components/customer/form.hbs'); expect($apiContact)->toContain('$contactCandidate->assertCustomerIdentityIsAvailable();'); expect($internalContact)->toContain('$this->assertCustomerIdentityIsAvailable($input'); expect($orderController)->toContain('$customerCandidate->assertCustomerIdentityIsAvailable();'); - expect($customerForm)->toContain('is_customer=true'); }); test('customer conflict audit command is registered and read only', function () { @@ -59,7 +57,6 @@ $contactModel = file_get_contents(__DIR__ . '/../src/Models/Contact.php'); $customerCredentials = file_get_contents(__DIR__ . '/../src/Mail/CustomerCredentialsMail.php'); $customerEmail = file_get_contents(__DIR__ . '/../resources/views/mail/customer-credentials.blade.php'); - $customerForm = file_get_contents(__DIR__ . '/../../addon/components/customer/form.hbs'); expect($internalContact) ->toContain('meta.customer_portal.send_welcome_email') @@ -81,9 +78,7 @@ ->and($customerEmail)->toContain('Your customer portal access is ready') ->and($customerEmail)->toContain('Sign in to customer portal') ->and($customerEmail)->toContain('Temporary password:') - ->and($customerEmail)->toContain('You can change your password after signing in.') - ->and($customerForm)->toContain('this.showWelcomeEmailOption') - ->and($customerForm)->toContain('this.toggleWelcomeEmail'); + ->and($customerEmail)->toContain('You can change your password after signing in.'); expect($contactModel) ->toContain('assignUserToContactCompany') diff --git a/server/tests/TelematicsHardeningTest.php b/server/tests/TelematicsHardeningTest.php index 6efcb276a..63962f249 100644 --- a/server/tests/TelematicsHardeningTest.php +++ b/server/tests/TelematicsHardeningTest.php @@ -86,20 +86,6 @@ ->toContain('meta->provider_account_id'); }); -test('device details render consistent telematics connection state and timestamp', function () { - $details = file_get_contents(__DIR__ . '/../../addon/components/device/details.hbs'); - $header = file_get_contents(__DIR__ . '/../../addon/components/device/panel-header.hbs'); - - expect($details) - ->toContain('format-date-fns this.lastSeenLabel "dd MMM yyyy, HH:mm"') - ->not->toContain('format-date @resource.last_online_at'); - - expect($header) - ->toContain('this.connectionStatus') - ->toContain('this.lastOnlineAt') - ->not->toContain('@resource.online "online"'); -}); - test('native providers normalize device payloads to canonical FleetOps keys', function () { $providers = [ file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/AfaqyProvider.php'), @@ -886,26 +872,6 @@ public function fetchDevicesForTest(): array ]); })->skip('Requires isolated Laravel HTTP client fake state in the full application harness.'); -test('telematics details use public id for consumer webhook URLs and do not read ember uuid', function () { - $component = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.js'); - $template = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.hbs'); - - expect($component) - ->toContain('const id = this.args.resource?.public_id;') - ->toContain('return null;') - ->not->toContain('this.args.resource?.uuid') - ->not->toContain('this.args.resource?.public_id ?? this.args.resource?.id'); - - expect($template) - ->toContain('this.hasWebhookUrl') - ->toContain('Webhook URL unavailable until this integration has a public ID.') - ->toContain('Provider polling'); - - expect($component) - ->toContain('last_sync_job_id') - ->toContain('last_sync_error'); -}); - test('native telematics providers expose local provider icons with a descriptor fallback', function () { $config = include __DIR__ . '/../config/telematics.php'; $iconPath = '/engines-dist/images/telematics/providers/'; @@ -1315,7 +1281,6 @@ public function fetchDevicesForTest(array $credentials): array test('telematics polling command is registered and scheduled for discovery providers by default', function () { $command = file_get_contents(__DIR__ . '/../src/Console/Commands/SyncTelematics.php'); $provider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php'); - $details = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.hbs'); expect($command) ->toContain("protected \$signature = 'fleetops:sync-telematics") @@ -1330,10 +1295,6 @@ public function fetchDevicesForTest(array $credentials): array expect($provider) ->toContain('Console\\Commands\\SyncTelematics::class') ->toContain("command('fleetops:sync-telematics')->everyMinute()"); - - expect($details) - ->toContain('Provider polling') - ->toContain('Fleet-Ops polls this provider for device snapshots and telemetry updates.'); }); test('native endpoint fields are advanced optional overrides with provider defaults', function () { @@ -1465,27 +1426,6 @@ public function fetchDevicesForTest(array $credentials): array ->not->toContain("'location',"); }); -test('telematics setup renders endpoint overrides only in advanced settings', function () { - $formTemplate = file_get_contents(__DIR__ . '/../../addon/components/telematic/form.hbs'); - $settingsTemplate = file_get_contents(__DIR__ . '/../../addon/components/telematic/settings.hbs'); - $formComponent = file_get_contents(__DIR__ . '/../../addon/components/telematic/form.js'); - $settingsComponent = file_get_contents(__DIR__ . '/../../addon/components/telematic/settings.js'); - - foreach ([$formComponent, $settingsComponent] as $component) { - expect($component) - ->toContain('advancedCredentialFields') - ->toContain('field.advanced || field.is_endpoint') - ->toContain('!field.advanced && !field.is_endpoint'); - } - - foreach ([$formTemplate, $settingsTemplate] as $template) { - expect($template) - ->toContain('Advanced connection settings') - ->toContain('this.advancedCredentialFields') - ->toContain('field.default_value'); - } -}); - test('telematics details logs are routed and use persisted safe data only', function () { $routes = file_get_contents(__DIR__ . '/../src/routes.php'); $controller = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/TelematicController.php'); @@ -1510,32 +1450,6 @@ public function fetchDevicesForTest(array $credentials): array ->not->toContain('Log::'); }); -test('telematics provider status display maps active and connected to connected', function () { - $indexController = file_get_contents(__DIR__ . '/../../addon/controllers/connectivity/telematics/index.js'); - $detailsController = file_get_contents(__DIR__ . '/../../addon/controllers/connectivity/telematics/details.js'); - $statusCell = file_get_contents(__DIR__ . '/../../addon/components/cell/telematic-status.js'); - - expect($indexController) - ->toContain("cellComponent: 'cell/telematic-status'"); - - expect($detailsController) - ->toContain("case 'active':\n return 'Connected';"); - - expect($statusCell) - ->toContain("case 'active':") - ->toContain("case 'connected':") - ->toContain("return 'Connected';"); -}); - -test('telematics overview attention items render as full width stacked alerts', function () { - $template = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.hbs'); - - expect($template) - ->toContain('
') - ->toContain('class="w-full rounded-md border border-yellow-200 bg-yellow-50 p-3') - ->not->toContain('lg:grid-cols-2'); -}); - test('device attachment morph types are normalized and legacy aliases are tolerated', function () { $migration = file_get_contents(__DIR__ . '/../migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php'); $command = file_get_contents(__DIR__ . '/../src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php'); diff --git a/tests/integration/components/customer/form-test.js b/tests/integration/components/customer/form-test.js index 2a161fb1c..907fee6e8 100644 --- a/tests/integration/components/customer/form-test.js +++ b/tests/integration/components/customer/form-test.js @@ -1,26 +1,100 @@ +import Component from '@glimmer/component'; +import Service from '@ember/service'; +import { setComponentTemplate } from '@ember/component'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'dummy/tests/helpers'; -import { render } from '@ember/test-helpers'; +import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; +let modelSelectQueries; + +class ModelSelectStub extends Component { + constructor() { + super(...arguments); + modelSelectQueries.push({ modelName: this.args.modelName, query: this.args.query }); + } +} + +class ExtensionManagerStub extends Service { + installed = []; + + isInstalled(name) { + return this.installed.includes(name); + } + + ensureEngineLoaded() { + return Promise.resolve(); + } +} + +function makeResource(initial = {}) { + return { + ...initial, + set(key, value) { + this[key] = value; + }, + setProperties(values) { + Object.assign(this, values); + }, + }; +} + module('Integration | Component | customer/form', function (hooks) { setupRenderingTest(hooks); - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); + hooks.beforeEach(function () { + modelSelectQueries = []; + + this.owner.register('service:universe/extension-manager', ExtensionManagerStub); + this.owner.register('component:model-select', setComponentTemplate(hbs`
`, ModelSelectStub)); + }); + + test('the user account selector only offers customer users without a contact', async function (assert) { + this.set('customer', makeResource({ isNew: true })); + + await render(hbs``); + + const userSelect = modelSelectQueries.find((entry) => entry.modelName === 'user'); + + assert.deepEqual(userSelect.query, { doesnt_have_contact: true, is_customer: true }); + }); + + test('the welcome email option is offered when creating a customer with the portal installed', async function (assert) { + this.owner.lookup('service:universe/extension-manager').installed = ['@fleetbase/customer-portal-engine']; + this.set('customer', makeResource({ isNew: true })); + + await render(hbs``); + + assert.dom('input[type="checkbox"]').exists({ count: 1 }); + assert.dom('input[type="checkbox"]').isNotChecked('the welcome email is opt in'); + assert.dom().includesText('Send customer portal welcome email'); + }); + + test('opting in stores the welcome email flag on the customer portal meta', async function (assert) { + this.owner.lookup('service:universe/extension-manager').installed = ['@fleetbase/customer-portal-engine']; + this.set('customer', makeResource({ isNew: true })); - await render(hbs``); + await render(hbs``); + await click('input[type="checkbox"]'); + + assert.deepEqual(this.customer.meta, { customer_portal: { send_welcome_email: true } }); + }); + + test('the welcome email option is hidden without the customer portal extension', async function (assert) { + this.set('customer', makeResource({ isNew: true })); + + await render(hbs``); + + assert.dom('input[type="checkbox"]').doesNotExist(); + assert.dom().doesNotIncludeText('Send customer portal welcome email'); + }); - assert.dom().hasText(''); + test('the welcome email option is hidden when editing an existing customer', async function (assert) { + this.owner.lookup('service:universe/extension-manager').installed = ['@fleetbase/customer-portal-engine']; + this.set('customer', makeResource({ isNew: false })); - // Template block usage: - await render(hbs` - - template block text - - `); + await render(hbs``); - assert.dom().hasText('template block text'); + assert.dom('input[type="checkbox"]').doesNotExist(); }); }); diff --git a/tests/integration/components/device/details-test.js b/tests/integration/components/device/details-test.js index f0b04b834..80e0af246 100644 --- a/tests/integration/components/device/details-test.js +++ b/tests/integration/components/device/details-test.js @@ -46,4 +46,29 @@ module('Integration | Component | device/details', function (hooks) { assert.dom().includesText('Geotab'); assert.dom().includesText('Online'); }); + + test('it renders the last seen timestamp in the shared telematics format', async function (assert) { + this.set('device', { + displayName: 'Gateway 101', + connection_status: 'online', + device_id: 'VG-101', + last_online_at: new Date(2026, 5, 18, 15, 28), + }); + + await render(hbs``); + + assert.dom().includesText('Last seen: 18 Jun 2026, 15:28'); + }); + + test('it renders a placeholder when the device has never reported', async function (assert) { + this.set('device', { + displayName: 'Gateway 101', + connection_status: 'never_connected', + device_id: 'VG-101', + }); + + await render(hbs``); + + assert.dom().includesText('Last seen: -'); + }); }); diff --git a/tests/integration/components/device/panel-header-test.js b/tests/integration/components/device/panel-header-test.js index 85a3f8c9e..aff627fee 100644 --- a/tests/integration/components/device/panel-header-test.js +++ b/tests/integration/components/device/panel-header-test.js @@ -15,13 +15,14 @@ module('Integration | Component | device/panel-header', function (hooks) { type: 'gps', imei: '864022084024776', attached_to_name: 'CLD-06', - last_online_at: '2026-06-18T15:28:00Z', + last_online_at: new Date(2026, 5, 18, 15, 28), }); await render(hbs``); assert.dom().includesText('BX-046'); assert.dom().includesText('Offline'); + assert.dom().includesText('18 Jun 2026, 15:28'); assert.dom().includesText('Afaqy'); assert.dom().includesText('864022084024776'); assert.dom().includesText('CLD-06'); @@ -42,6 +43,19 @@ module('Integration | Component | device/panel-header', function (hooks) { assert.dom().includesText('No last online'); }); + test('it prefers the telematics connection state over the raw online flag', async function (assert) { + this.set('resource', { + serial_number: 'SN-200', + connection_status: 'recently_offline', + online: true, + is_online: true, + }); + + await render(hbs``); + + assert.dom().includesText('Recently Offline'); + }); + test('it renders safe fallbacks without a resource', async function (assert) { await render(hbs``); diff --git a/tests/integration/components/telematic/details-test.js b/tests/integration/components/telematic/details-test.js index 71d52ae09..89740d008 100644 --- a/tests/integration/components/telematic/details-test.js +++ b/tests/integration/components/telematic/details-test.js @@ -6,21 +6,91 @@ import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | telematic/details', function (hooks) { setupRenderingTest(hooks); - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); + test('it renders the consumer webhook url from the public id', async function (assert) { + this.set('telematic', { + public_id: 'telematic_abc123', + provider_descriptor: { + label: 'Samsara', + supports_webhooks: true, + webhook_url: 'https://api.example.test/telematics/webhook', + }, + }); - await render(hbs``); + await render(hbs``); - assert.dom().hasText(''); + assert.dom().includesText('Webhook URL'); + assert.dom('input[readonly]').hasValue('https://api.example.test/telematics/webhook?telematic=telematic_abc123'); + assert.dom().doesNotIncludeText('Webhook URL unavailable until this integration has a public ID.'); + }); + + test('it explains that the webhook url is unavailable before a public id exists', async function (assert) { + this.set('telematic', { + id: '0f7a2c1e-0000-4000-8000-000000000000', + uuid: '0f7a2c1e-0000-4000-8000-000000000000', + provider_descriptor: { + label: 'Samsara', + supports_webhooks: true, + webhook_url: 'https://api.example.test/telematics/webhook', + }, + }); + + await render(hbs``); + + assert.dom().includesText('Webhook URL unavailable until this integration has a public ID.'); + assert.dom('input[readonly]').doesNotExist(); + }); + + test('it describes polling for providers without webhook support', async function (assert) { + this.set('telematic', { + public_id: 'telematic_abc123', + provider_descriptor: { + label: 'Afaqy', + supports_webhooks: false, + }, + }); + + await render(hbs``); + + assert.dom().includesText('Provider polling'); + assert.dom().includesText('Fleet-Ops polls this provider for device snapshots and telemetry updates.'); + assert.dom().doesNotIncludeText('Webhook URL'); + }); + + test('it stacks attention items as full width alerts', async function (assert) { + this.set('telematic', { + public_id: 'telematic_abc123', + provider_descriptor: { label: 'Samsara', supports_webhooks: true, webhook_url: 'https://api.example.test/hook' }, + meta: { + last_error: 'Connection refused by the provider.', + last_sync_error: 'Device index returned HTTP 401.', + unattached_devices_count: 3, + }, + }); + + await render(hbs``); + + const alerts = this.element.querySelectorAll('.bg-yellow-50'); + + assert.strictEqual(alerts.length, 3, 'every attention item renders its own alert'); + alerts.forEach((alert) => assert.dom(alert).hasClass('w-full')); + + const alertList = alerts[0].parentElement; + + assert.dom(alertList).hasClass('flex'); + assert.dom(alertList).hasClass('flex-col'); + assert.dom(alertList).hasClass('gap-3'); + assert.strictEqual(this.element.querySelector('.lg\\:grid-cols-2'), null, 'attention items are never laid out in a two column grid'); + }); + + test('it confirms when nothing needs attention', async function (assert) { + this.set('telematic', { + public_id: 'telematic_abc123', + provider_descriptor: { label: 'Samsara', supports_webhooks: true, webhook_url: 'https://api.example.test/hook' }, + }); - // Template block usage: - await render(hbs` - - template block text - - `); + await render(hbs``); - assert.dom().hasText('template block text'); + assert.dom().includesText('No immediate attention needed'); + assert.strictEqual(this.element.querySelectorAll('.bg-yellow-50').length, 0); }); }); diff --git a/tests/integration/components/telematic/form-test.js b/tests/integration/components/telematic/form-test.js index 1e3b4680f..f84f35093 100644 --- a/tests/integration/components/telematic/form-test.js +++ b/tests/integration/components/telematic/form-test.js @@ -1,26 +1,94 @@ +import Service from '@ember/service'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'dummy/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; +const SAFEE_DESCRIPTOR = { + key: 'safee', + label: 'Safee', + type: 'native', + required_fields: [ + { name: 'username', label: 'Username', required: true }, + { name: 'password', label: 'Password', required: true, type: 'password' }, + { name: 'server_uri', label: 'Server URI', advanced: true, is_endpoint: true, required: false, default_value: 'https://api.safee.com' }, + ], +}; + +const FLESPI_DESCRIPTOR = { + key: 'flespi', + label: 'Flespi', + type: 'native', + required_fields: [{ name: 'token', label: 'Token', required: true }], +}; + +class FetchStub extends Service { + get() { + return Promise.resolve([SAFEE_DESCRIPTOR, FLESPI_DESCRIPTOR]); + } +} + +class NotificationsStub extends Service { + serverError() {} +} + +function makeResource(initial = {}) { + return { + ...initial, + set(key, value) { + this[key] = value; + }, + setProperties(values) { + Object.assign(this, values); + }, + }; +} + module('Integration | Component | telematic/form', function (hooks) { setupRenderingTest(hooks); - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); + hooks.beforeEach(function () { + this.owner.register('service:fetch', FetchStub); + this.owner.register('service:notifications', NotificationsStub); + }); + + test('endpoint overrides render inside the advanced section with provider defaults', async function (assert) { + this.set( + 'telematic', + makeResource({ + provider: 'safee', + provider_descriptor: SAFEE_DESCRIPTOR, + credentials: { username: null, password: null, server_uri: null }, + }) + ); - await render(hbs``); + await render(hbs``); + + assert.dom('details summary').includesText('Advanced connection settings'); + assert.deepEqual( + [...this.element.querySelectorAll('details input')].map((input) => input.value), + ['https://api.safee.com'], + 'the endpoint override falls back to the provider default' + ); + assert.dom('details').includesText('Server URI'); + assert.dom('details').doesNotIncludeText('Username'); + assert.dom().includesText('Username'); + }); - assert.dom().hasText(''); + test('providers without endpoint overrides do not render the advanced section', async function (assert) { + this.set( + 'telematic', + makeResource({ + provider: 'flespi', + provider_descriptor: FLESPI_DESCRIPTOR, + credentials: { token: null }, + }) + ); - // Template block usage: - await render(hbs` - - template block text - - `); + await render(hbs``); - assert.dom().hasText('template block text'); + assert.dom('details').doesNotExist(); + assert.dom().doesNotIncludeText('Advanced connection settings'); + assert.dom().includesText('Token'); }); }); diff --git a/tests/integration/components/telematic/settings-test.js b/tests/integration/components/telematic/settings-test.js new file mode 100644 index 000000000..62c1a5147 --- /dev/null +++ b/tests/integration/components/telematic/settings-test.js @@ -0,0 +1,89 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +const SAFEE_DESCRIPTOR = { + key: 'safee', + label: 'Safee', + required_fields: [ + { name: 'username', label: 'Username', required: true }, + { name: 'password', label: 'Password', required: true, type: 'password' }, + { name: 'server_uri', label: 'Server URI', advanced: true, is_endpoint: true, required: false, default_value: 'https://api.safee.com' }, + ], +}; + +function makeResource(initial = {}) { + return { + ...initial, + set(key, value) { + this[key] = value; + }, + }; +} + +module('Integration | Component | telematic/settings', function (hooks) { + setupRenderingTest(hooks); + + test('endpoint overrides render inside the advanced section with provider defaults', async function (assert) { + this.set( + 'telematic', + makeResource({ + provider: 'safee', + public_id: 'telematic_abc123', + provider_descriptor: SAFEE_DESCRIPTOR, + credentials: { username: 'fleet-ops', password: null }, + }) + ); + + await render(hbs``); + + assert.dom('details summary').includesText('Advanced connection settings'); + assert.deepEqual( + [...this.element.querySelectorAll('details input')].map((input) => input.value), + ['https://api.safee.com'], + 'the endpoint override falls back to the provider default' + ); + assert.dom('details').includesText('Server URI'); + assert.dom('details').doesNotIncludeText('Username'); + }); + + test('a saved endpoint override wins over the provider default', async function (assert) { + this.set( + 'telematic', + makeResource({ + provider: 'safee', + provider_descriptor: SAFEE_DESCRIPTOR, + credentials: { username: 'fleet-ops', server_uri: 'https://fms.example.test' }, + }) + ); + + await render(hbs``); + + assert.deepEqual( + [...this.element.querySelectorAll('details input')].map((input) => input.value), + ['https://fms.example.test'] + ); + }); + + test('providers without endpoint overrides do not render the advanced section', async function (assert) { + this.set( + 'telematic', + makeResource({ + provider: 'flespi', + provider_descriptor: { + key: 'flespi', + label: 'Flespi', + required_fields: [{ name: 'token', label: 'Token', required: true }], + }, + credentials: { token: 'abc' }, + }) + ); + + await render(hbs``); + + assert.dom('details').doesNotExist(); + assert.dom().doesNotIncludeText('Advanced connection settings'); + assert.dom().includesText('Token'); + }); +}); diff --git a/tests/unit/components/cell/telematic-status-test.js b/tests/unit/components/cell/telematic-status-test.js new file mode 100644 index 000000000..cf103ef16 --- /dev/null +++ b/tests/unit/components/cell/telematic-status-test.js @@ -0,0 +1,33 @@ +import { module, test } from 'qunit'; +import CellTelematicStatusComponent from 'dummy/components/cell/telematic-status'; + +function makeStatusCell(status) { + const component = Object.create(CellTelematicStatusComponent.prototype); + component.args = { row: { status } }; + + return component; +} + +module('Unit | Component | cell/telematic-status', function () { + test('active and connected both display as connected', function (assert) { + assert.strictEqual(makeStatusCell('active').label, 'Connected'); + assert.strictEqual(makeStatusCell('connected').label, 'Connected'); + assert.strictEqual(makeStatusCell('active').badgeStatus, 'success'); + assert.strictEqual(makeStatusCell('connected').badgeStatus, 'success'); + }); + + test('remaining provider statuses map to their operator facing labels', function (assert) { + assert.strictEqual(makeStatusCell('synchronizing').label, 'Syncing'); + assert.strictEqual(makeStatusCell('synchronizing').badgeStatus, 'info'); + assert.strictEqual(makeStatusCell('initialized').label, 'Not tested'); + assert.strictEqual(makeStatusCell('error').label, 'Needs attention'); + assert.strictEqual(makeStatusCell('error').badgeStatus, 'warning'); + assert.strictEqual(makeStatusCell('degraded').badgeStatus, 'warning'); + assert.strictEqual(makeStatusCell('disconnected').badgeStatus, 'warning'); + }); + + test('a missing status falls back to unknown', function (assert) { + assert.strictEqual(makeStatusCell(undefined).label, 'Unknown'); + assert.strictEqual(makeStatusCell(undefined).badgeStatus, 'default'); + }); +}); diff --git a/tests/unit/components/customer/form-test.js b/tests/unit/components/customer/form-test.js new file mode 100644 index 000000000..72b166bc4 --- /dev/null +++ b/tests/unit/components/customer/form-test.js @@ -0,0 +1,78 @@ +import { module, test } from 'qunit'; +import CustomerFormComponent from 'dummy/components/customer/form'; + +function makeResource(initial = {}) { + return { + ...initial, + set(key, value) { + this[key] = value; + }, + }; +} + +function makeForm(resource, { installedExtensions = [] } = {}) { + return Object.create(CustomerFormComponent.prototype, { + args: { value: { resource } }, + extensionManager: { + value: { + isInstalled: (name) => installedExtensions.includes(name), + }, + }, + }); +} + +module('Unit | Component | customer/form', function () { + test('the welcome email option is offered when creating a customer with the portal installed', function (assert) { + const component = makeForm(makeResource({ isNew: true }), { installedExtensions: ['@fleetbase/customer-portal-engine'] }); + + assert.true(component.showWelcomeEmailOption); + }); + + test('the welcome email option is hidden without the customer portal extension', function (assert) { + const component = makeForm(makeResource({ isNew: true })); + + assert.false(component.showWelcomeEmailOption); + }); + + test('the welcome email option is hidden when editing an existing customer', function (assert) { + const component = makeForm(makeResource({ isNew: false }), { installedExtensions: ['@fleetbase/customer-portal-engine'] }); + + assert.false(component.showWelcomeEmailOption); + }); + + test('the welcome email is opt in and defaults to off', function (assert) { + assert.false(makeForm(makeResource({ isNew: true })).sendWelcomeEmail); + assert.false(makeForm(makeResource({ isNew: true, meta: {} })).sendWelcomeEmail); + assert.true(makeForm(makeResource({ meta: { customer_portal: { send_welcome_email: true } } })).sendWelcomeEmail); + }); + + test('toggling the welcome email writes the opt in flag without dropping other meta', function (assert) { + const resource = makeResource({ isNew: true, meta: { customer_portal: { access_url_slug: 'acme' }, source: 'console' } }); + const component = makeForm(resource, { installedExtensions: ['@fleetbase/customer-portal-engine'] }); + + component.toggleWelcomeEmail(true); + + assert.deepEqual(resource.meta, { + source: 'console', + customer_portal: { + access_url_slug: 'acme', + send_welcome_email: true, + }, + }); + assert.true(component.sendWelcomeEmail); + + component.toggleWelcomeEmail(false); + + assert.false(component.sendWelcomeEmail); + assert.strictEqual(resource.meta.customer_portal.access_url_slug, 'acme'); + }); + + test('toggling without an argument flips the current opt in value', function (assert) { + const resource = makeResource({ isNew: true }); + const component = makeForm(resource, { installedExtensions: ['@fleetbase/customer-portal-engine'] }); + + component.toggleWelcomeEmail(); + + assert.true(component.sendWelcomeEmail); + }); +}); diff --git a/tests/unit/components/device/details-test.js b/tests/unit/components/device/details-test.js new file mode 100644 index 000000000..7ce2d5bd8 --- /dev/null +++ b/tests/unit/components/device/details-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'qunit'; +import DeviceDetailsComponent from 'dummy/components/device/details'; + +function makeDetails(resource) { + const component = Object.create(DeviceDetailsComponent.prototype); + component.args = { resource }; + + return component; +} + +module('Unit | Component | device/details', function () { + test('last seen label is read from either payload casing', function (assert) { + assert.strictEqual(makeDetails({ last_online_at: '2026-06-18T15:28:00Z' }).lastSeenLabel, '2026-06-18T15:28:00Z'); + assert.strictEqual(makeDetails({ lastOnlineAt: '2026-06-18T15:28:00Z' }).lastSeenLabel, '2026-06-18T15:28:00Z'); + assert.strictEqual(makeDetails({}).lastSeenLabel, undefined); + }); + + test('connection status prefers the telematics connection state over the online flag', function (assert) { + assert.strictEqual(makeDetails({ connection_status: 'long_offline', is_online: true }).connectionStatus, 'long_offline'); + assert.strictEqual(makeDetails({ is_online: true }).connectionStatus, 'online'); + assert.strictEqual(makeDetails({ is_online: false }).connectionStatus, 'offline'); + }); +}); diff --git a/tests/unit/components/device/panel-header-test.js b/tests/unit/components/device/panel-header-test.js new file mode 100644 index 000000000..5d55b6f70 --- /dev/null +++ b/tests/unit/components/device/panel-header-test.js @@ -0,0 +1,35 @@ +import { module, test } from 'qunit'; +import DevicePanelHeaderComponent from 'dummy/components/device/panel-header'; + +function makePanelHeader(resource) { + const component = Object.create(DevicePanelHeaderComponent.prototype); + component.args = { resource }; + + return component; +} + +module('Unit | Component | device/panel-header', function () { + test('connection status prefers the telematics connection state', function (assert) { + const component = makePanelHeader({ + connection_status: 'recently_offline', + status: 'active', + is_online: true, + }); + + assert.strictEqual(component.connectionStatus, 'recently_offline'); + }); + + test('connection status falls back to the record status then to the online flag', function (assert) { + assert.strictEqual(makePanelHeader({ status: 'active', is_online: false }).connectionStatus, 'active'); + assert.strictEqual(makePanelHeader({ is_online: true }).connectionStatus, 'online'); + assert.strictEqual(makePanelHeader({ online: true }).connectionStatus, 'online'); + assert.strictEqual(makePanelHeader({ is_online: false }).connectionStatus, 'offline'); + assert.strictEqual(makePanelHeader(undefined).connectionStatus, 'offline'); + }); + + test('last online timestamp is read from either payload casing', function (assert) { + assert.strictEqual(makePanelHeader({ last_online_at: '2026-06-18T15:28:00Z' }).lastOnlineAt, '2026-06-18T15:28:00Z'); + assert.strictEqual(makePanelHeader({ lastOnlineAt: '2026-06-18T15:28:00Z' }).lastOnlineAt, '2026-06-18T15:28:00Z'); + assert.strictEqual(makePanelHeader({}).lastOnlineAt, undefined); + }); +}); diff --git a/tests/unit/components/telematic/details-test.js b/tests/unit/components/telematic/details-test.js new file mode 100644 index 000000000..8a8ad2964 --- /dev/null +++ b/tests/unit/components/telematic/details-test.js @@ -0,0 +1,80 @@ +import { module, test } from 'qunit'; +import TelematicDetailsComponent from 'dummy/components/telematic/details'; + +function makeDetails(resource) { + const component = Object.create(TelematicDetailsComponent.prototype); + component.args = { resource }; + + return component; +} + +module('Unit | Component | telematic/details', function () { + test('webhook url is built from the public id', function (assert) { + const component = makeDetails({ + public_id: 'telematic_abc123', + uuid: '0f7a2c1e-0000-4000-8000-000000000000', + provider_descriptor: { webhook_url: 'https://api.example.test/telematics/webhook' }, + }); + + assert.strictEqual(component.webhookUrl, 'https://api.example.test/telematics/webhook?telematic=telematic_abc123'); + assert.true(component.hasWebhookUrl); + }); + + test('webhook url appends to an existing query string', function (assert) { + const component = makeDetails({ + public_id: 'telematic_abc123', + provider_descriptor: { webhook_url: 'https://api.example.test/telematics/webhook?provider=samsara' }, + }); + + assert.strictEqual(component.webhookUrl, 'https://api.example.test/telematics/webhook?provider=samsara&telematic=telematic_abc123'); + }); + + test('webhook url is unavailable when the record only has ember identifiers', function (assert) { + const component = makeDetails({ + id: '0f7a2c1e-0000-4000-8000-000000000000', + uuid: '0f7a2c1e-0000-4000-8000-000000000000', + provider_descriptor: { webhook_url: 'https://api.example.test/telematics/webhook' }, + }); + + assert.strictEqual(component.webhookUrl, null, 'the ember uuid is never used as the public webhook identifier'); + assert.false(component.hasWebhookUrl); + }); + + test('webhook url is unavailable when the provider has no webhook endpoint', function (assert) { + const component = makeDetails({ public_id: 'telematic_abc123', provider_descriptor: {} }); + + assert.strictEqual(component.webhookUrl, null); + assert.false(component.hasWebhookUrl); + }); + + test('the devices synced card surfaces the last sync job id', function (assert) { + const component = makeDetails({ + meta: { last_sync_total: 12, last_sync_job_id: 'job_9f21' }, + }); + const devicesSynced = component.healthCards.find((card) => card.label === 'Devices synced'); + + assert.strictEqual(devicesSynced.value, 12); + assert.strictEqual(devicesSynced.detail, 'job_9f21'); + assert.false(devicesSynced.detailIsDate); + }); + + test('a last sync error is surfaced as an attention item', function (assert) { + const component = makeDetails({ + meta: { last_sync_error: 'Provider returned HTTP 401 for the device index.' }, + }); + + assert.deepEqual( + component.attentionItems.map((item) => item.title), + ['Sync issue'] + ); + assert.strictEqual(component.attentionItems[0].description, 'Provider returned HTTP 401 for the device index.'); + }); + + test('sensitive sync errors are replaced with a safe operator message', function (assert) { + const component = makeDetails({ + meta: { last_sync_error: 'SQLSTATE[42S02]: Base table or view not found' }, + }); + + assert.strictEqual(component.attentionItems[0].description, 'Device sync failed. Review the provider connection and server logs, then try again.'); + }); +}); diff --git a/tests/unit/components/telematic/form-test.js b/tests/unit/components/telematic/form-test.js index 6d26595f1..0d29b2304 100644 --- a/tests/unit/components/telematic/form-test.js +++ b/tests/unit/components/telematic/form-test.js @@ -13,6 +13,12 @@ function makeResource(initial = {}) { }; } +function makeFormWithProvider(provider) { + return Object.create(TelematicFormComponent.prototype, { + selectedProvider: { value: provider, writable: true }, + }); +} + module('Unit | Component | telematic/form', function () { test('provider credential defaults include advanced endpoint values', function (assert) { const provider = { @@ -32,6 +38,34 @@ module('Unit | Component | telematic/form', function () { }); }); + test('endpoint and advanced fields are separated from the primary credential fields', function (assert) { + const component = makeFormWithProvider({ + required_fields: [ + { name: 'api_key' }, + { name: 'server_uri', advanced: true, is_endpoint: true, default_value: 'https://api.safee.com' }, + { name: 'base_url', is_endpoint: true, default_value: 'https://api.afaqy.com' }, + { name: 'realm_id', advanced: true }, + ], + }); + + assert.deepEqual( + component.credentialFields.map((field) => field.name), + ['api_key'] + ); + assert.deepEqual( + component.advancedCredentialFields.map((field) => field.name), + ['server_uri', 'base_url', 'realm_id'] + ); + assert.true(component.hasAdvancedCredentialFields); + }); + + test('providers without advanced fields do not render the advanced section', function (assert) { + const component = makeFormWithProvider({ required_fields: [{ name: 'api_key' }] }); + + assert.deepEqual(component.advancedCredentialFields, []); + assert.false(component.hasAdvancedCredentialFields); + }); + test('editing server uri updates resource credentials', function (assert) { const resource = makeResource({ credentials: { diff --git a/tests/unit/components/telematic/settings-test.js b/tests/unit/components/telematic/settings-test.js new file mode 100644 index 000000000..7ca40200a --- /dev/null +++ b/tests/unit/components/telematic/settings-test.js @@ -0,0 +1,58 @@ +import { module, test } from 'qunit'; +import TelematicSettingsComponent from 'dummy/components/telematic/settings'; + +const REQUIRED_FIELDS = [ + { name: 'api_key', label: 'API Key' }, + { name: 'server_uri', label: 'Server URI', advanced: true, is_endpoint: true, default_value: 'https://api.safee.com' }, + { name: 'base_url', label: 'Base URL', is_endpoint: true, default_value: 'https://api.afaqy.com' }, + { name: 'realm_id', label: 'Realm', advanced: true }, +]; + +function makeSettings(resource) { + const component = Object.create(TelematicSettingsComponent.prototype); + component.args = { resource }; + + return component; +} + +module('Unit | Component | telematic/settings', function () { + test('endpoint and advanced fields are separated from the primary credential fields', function (assert) { + const component = makeSettings({ provider_descriptor: { required_fields: REQUIRED_FIELDS } }); + + assert.deepEqual( + component.credentialFields.map((field) => field.name), + ['api_key'] + ); + assert.deepEqual( + component.advancedCredentialFields.map((field) => field.name), + ['server_uri', 'base_url', 'realm_id'] + ); + assert.true(component.hasAdvancedCredentialFields); + }); + + test('providers without advanced fields do not render the advanced section', function (assert) { + const component = makeSettings({ provider_descriptor: { required_fields: [{ name: 'api_key' }] } }); + + assert.deepEqual(component.advancedCredentialFields, []); + assert.false(component.hasAdvancedCredentialFields); + }); + + test('a provider without a descriptor is safe to render', function (assert) { + const component = makeSettings({}); + + assert.deepEqual(component.credentialFields, []); + assert.deepEqual(component.advancedCredentialFields, []); + assert.false(component.hasAdvancedCredentialFields); + }); + + test('webhook url requires both a provider endpoint and a public id', function (assert) { + const provider = { webhook_url: 'https://api.example.test/telematics/webhook' }; + + assert.strictEqual( + makeSettings({ provider_descriptor: provider, public_id: 'telematic_abc123' }).webhookUrl, + 'https://api.example.test/telematics/webhook?telematic=telematic_abc123' + ); + assert.strictEqual(makeSettings({ provider_descriptor: provider }).webhookUrl, null); + assert.false(makeSettings({ provider_descriptor: provider }).hasWebhookUrl); + }); +}); diff --git a/tests/unit/controllers/connectivity/telematics/details-test.js b/tests/unit/controllers/connectivity/telematics/details-test.js new file mode 100644 index 000000000..fd6a1ff6d --- /dev/null +++ b/tests/unit/controllers/connectivity/telematics/details-test.js @@ -0,0 +1,55 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +module('Unit | Controller | connectivity/telematics/details', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.controller = this.owner.lookup('controller:connectivity/telematics/details'); + }); + + test('active and connected both display as connected', function (assert) { + this.controller.model = { status: 'active' }; + assert.strictEqual(this.controller.statusLabel, 'Connected'); + assert.strictEqual(this.controller.healthStatus, 'success'); + + this.controller.model = { status: 'connected' }; + assert.strictEqual(this.controller.statusLabel, 'Connected'); + assert.strictEqual(this.controller.healthStatus, 'success'); + }); + + test('remaining provider statuses map to their operator facing labels', function (assert) { + this.controller.model = { status: 'synchronizing' }; + assert.strictEqual(this.controller.statusLabel, 'Syncing'); + assert.strictEqual(this.controller.healthStatus, 'info'); + + this.controller.model = { status: 'initialized' }; + assert.strictEqual(this.controller.statusLabel, 'Not tested'); + assert.strictEqual(this.controller.healthStatus, 'default'); + + this.controller.model = { status: 'error' }; + assert.strictEqual(this.controller.statusLabel, 'Needs attention'); + assert.strictEqual(this.controller.healthStatus, 'warning'); + }); + + test('a missing status falls back to unknown', function (assert) { + this.controller.model = {}; + + assert.strictEqual(this.controller.statusLabel, 'Unknown'); + assert.strictEqual(this.controller.healthStatus, 'default'); + }); + + test('the last sync timestamp follows the sync lifecycle', function (assert) { + this.controller.model = { + status: 'synchronizing', + meta: { last_sync_started_at: '2026-06-18T15:00:00Z', last_sync_completed_at: '2026-06-17T15:00:00Z' }, + }; + assert.strictEqual(this.controller.lastSyncAt, '2026-06-18T15:00:00Z'); + + this.controller.model = { + status: 'connected', + meta: { last_sync_started_at: '2026-06-18T15:00:00Z', last_sync_completed_at: '2026-06-18T15:04:00Z' }, + }; + assert.strictEqual(this.controller.lastSyncAt, '2026-06-18T15:04:00Z'); + }); +}); diff --git a/tests/unit/controllers/connectivity/telematics/index-test.js b/tests/unit/controllers/connectivity/telematics/index-test.js index a49e4ce71..096d53c42 100644 --- a/tests/unit/controllers/connectivity/telematics/index-test.js +++ b/tests/unit/controllers/connectivity/telematics/index-test.js @@ -4,9 +4,15 @@ import { setupTest } from 'dummy/tests/helpers'; module('Unit | Controller | connectivity/telematics/index', function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. test('it exists', function (assert) { let controller = this.owner.lookup('controller:connectivity/telematics/index'); assert.ok(controller); }); + + test('the status column renders through the shared telematic status cell', function (assert) { + let controller = this.owner.lookup('controller:connectivity/telematics/index'); + let statusColumn = controller.columns.find((column) => column.valuePath === 'status'); + + assert.strictEqual(statusColumn.cellComponent, 'cell/telematic-status'); + }); });