From 727b3f1752506ed18dc1560c4e28100d1ab361b8 Mon Sep 17 00:00:00 2001 From: Tommy Carter Date: Fri, 7 Mar 2025 12:07:32 -0600 Subject: [PATCH 1/3] Added selection component tests --- .../tests/helpers/{index.js => index.ts} | 11 ++- apps/tutorial/tests/helpers/mocks.ts | 92 +++++++++++++++++++ .../integration/components/selection-test.gts | 75 +++++++++++++++ 3 files changed, 173 insertions(+), 5 deletions(-) rename apps/tutorial/tests/helpers/{index.js => index.ts} (74%) create mode 100644 apps/tutorial/tests/helpers/mocks.ts create mode 100644 apps/tutorial/tests/integration/components/selection-test.gts diff --git a/apps/tutorial/tests/helpers/index.js b/apps/tutorial/tests/helpers/index.ts similarity index 74% rename from apps/tutorial/tests/helpers/index.js rename to apps/tutorial/tests/helpers/index.ts index 7f70de80f..e190f567e 100644 --- a/apps/tutorial/tests/helpers/index.js +++ b/apps/tutorial/tests/helpers/index.ts @@ -2,13 +2,14 @@ import { setupApplicationTest as upstreamSetupApplicationTest, setupRenderingTest as upstreamSetupRenderingTest, setupTest as upstreamSetupTest, + type SetupTestOptions, } from 'ember-qunit'; -// This file exists to provide wrappers around ember-qunit's / ember-mocha's +// This file exists to provide wrappers around ember-qunit's // test setup functions. This way, you can easily extend the setup that is // needed per test type. -function setupApplicationTest(hooks, options) { +function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { upstreamSetupApplicationTest(hooks, options); // Additional setup for application tests can be done here. @@ -23,17 +24,17 @@ function setupApplicationTest(hooks, options) { // This is also a good place to call test setup functions coming // from other addons: // - // setupIntl(hooks); // ember-intl + // setupIntl(hooks, 'en-us'); // ember-intl // setupMirage(hooks); // ember-cli-mirage } -function setupRenderingTest(hooks, options) { +function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { upstreamSetupRenderingTest(hooks, options); // Additional setup for rendering tests can be done here. } -function setupTest(hooks, options) { +function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { upstreamSetupTest(hooks, options); // Additional setup for unit tests can be done here. diff --git a/apps/tutorial/tests/helpers/mocks.ts b/apps/tutorial/tests/helpers/mocks.ts new file mode 100644 index 000000000..33ff8d946 --- /dev/null +++ b/apps/tutorial/tests/helpers/mocks.ts @@ -0,0 +1,92 @@ +import Service from '@ember/service'; + +/** + * Build the structure used by the docs service for mocking. + */ +function makeDocsTree(treeData: [string, string[]][]) { + const pages = []; + + for (const [groupName, tutorialNames] of treeData) { + const groupPath = groupName + .toLowerCase() + .replace(/ /g, '-') + .replace(/[^a-z0-9-]/g, ''); + + const groupPages = []; + + for (const tutorialName of tutorialNames) { + const tutorialPath = tutorialName + .toLowerCase() + .replace(/ /g, '-') + .replace(/[^a-z0-9-]/g, ''); + + const prosePath = `/${groupPath}/${tutorialPath}/prose.md`; + + const page = { + path: tutorialPath, + name: tutorialName, + cleanedName: tutorialName.replace(/-/g, ' '), + pages: [ + { + path: prosePath, + name: 'prose', + groupName: tutorialName.replace(/-/g, ' '), + cleanedName: 'prose', + }, + ], + first: prosePath, + }; + + groupPages.push(page); + } + const group = { + path: groupPath, + name: groupName, + cleanedName: groupName.replace(/-/g, ' '), + pages: groupPages, + first: `/${groupPath}/${groupPages[0]?.path}/prose.md`, + }; + pages.push(group); + } + + return { + name: 'root', + pages, + path: 'root', + first: `/${pages[0]?.path}/${pages[0]?.pages[0]?.path}/prose.md`, + }; +} + +export class MockDocsService extends Service { + #groupsTree = {}; + #currentPath = '/'; + + get grouped() { + return this.#groupsTree; + } + + get currentPath() { + return this.#currentPath; + } + + // Test extension + _setGroupsData(treeData: [string, string[]][]) { + this.#groupsTree = makeDocsTree(treeData); + } + + _setCurrentPath(path: string) { + this.#currentPath = path; + } +} + +export class MockRouterService extends Service { + transitionTo(newRoute: string) { + this._assert.step(`transition to: ${newRoute}`); + } + + // Test extension + _assert!: Assert; + _setAssert(assert: Assert) { + this._assert = assert; + } +} diff --git a/apps/tutorial/tests/integration/components/selection-test.gts b/apps/tutorial/tests/integration/components/selection-test.gts new file mode 100644 index 000000000..0271e0505 --- /dev/null +++ b/apps/tutorial/tests/integration/components/selection-test.gts @@ -0,0 +1,75 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'tutorial/tests/helpers'; +import { render, select } from '@ember/test-helpers'; +import { Selection } from 'tutorial/components/selection'; +import { MockDocsService, MockRouterService } from '../../helpers/mocks'; + +module('Integration | Component | selection', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function (assert) { + this.owner.register('service:docs', MockDocsService); + this.owner.register('service:router', MockRouterService); + }); + + test('it renders groups and options', async function (assert) { + const docsSvc = this.owner.lookup('service:docs') as unknown as MockDocsService; + docsSvc._setGroupsData([ + [ + '1-introduction', + ['1-basics', '2-adding-data', '3-transforming-data', '4-multiple-transforms'], + ], + ['2-reactivity', ['1-values', '2-decorated-values', '3-derived-values']], + ['3-event-handling', ['1-dom-events']], + ]); + + await render(); + assert.dom('select').hasAttribute('name', 'tutorial'); + assert.dom('select > optgroup').exists({ count: 3 }); + assert.dom('option').exists({ count: 8 }); + // Check group labels + assert.dom('optgroup:nth-child(1)').hasAttribute('label', 'Introduction'); + assert.dom('optgroup:nth-child(2)').hasAttribute('label', 'Reactivity'); + assert.dom('optgroup:nth-child(3)').hasAttribute('label', 'Event Handling'); + // Spot check some option values + assert + .dom('optgroup:nth-child(1) option:nth-child(4)') + .hasValue('/1-introduction/4-multiple-transforms'); + assert.dom('optgroup:nth-child(1) option:nth-child(4)').hasText('Multiple Transforms'); + assert + .dom('optgroup:nth-child(2) option:nth-child(2)') + .hasValue('/2-reactivity/2-decorated-values'); + assert.dom('optgroup:nth-child(2) option:nth-child(2)').hasText('Decorated Values'); + }); + + test('it handles selection', async function (assert) { + const routerSvc = this.owner.lookup('service:router') as unknown as MockRouterService; + routerSvc._setAssert(assert); + const docsSvc = this.owner.lookup('service:docs') as unknown as MockDocsService; + docsSvc._setGroupsData([ + ['english', ['one', 'two', 'three']], + ['spanish', ['uno', 'dos', 'tres']], + ['german', ['eins', 'zwei', 'drei']], + ]); + docsSvc._setCurrentPath('/spanish/dos'); + + await render(); + assert.dom('select').hasAttribute('name', 'tutorial'); + assert.dom('select > optgroup').exists({ count: 3 }); + assert.dom('option').exists({ count: 9 }); + + assert.dom('select').hasValue('/spanish/dos'); + // Not sure why, but `:checked` is how you get selected option + assert.dom('option:checked').hasValue('/spanish/dos').hasText('Dos'); + + await select('select', '/german/eins'); + docsSvc._setCurrentPath('/german/eins'); + assert.dom('option:checked').hasValue('/german/eins').hasText('Eins'); + + await select('select', '/english/three'); + docsSvc._setCurrentPath('/english/three'); + assert.dom('option:checked').hasValue('/english/three').hasText('Three'); + + assert.verifySteps(['transition to: /german/eins', 'transition to: /english/three']); + }); +}); From 3cb448adde8d169864a51cad56e3e6065cd539fb Mon Sep 17 00:00:00 2001 From: Tommy Carter Date: Fri, 7 Mar 2025 12:50:00 -0600 Subject: [PATCH 2/3] Fixed Selection component --- apps/tutorial/app/components/selection.gts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/tutorial/app/components/selection.gts b/apps/tutorial/app/components/selection.gts index 351806db4..b08ced67f 100644 --- a/apps/tutorial/app/components/selection.gts +++ b/apps/tutorial/app/components/selection.gts @@ -29,8 +29,9 @@ export class Selection extends Component { this.router.transitionTo(event.target.value); }; - isSelected = ({ path }: { path: string }) => { - return this.docs.currentPath === path; + isSelected = (group: { path: string }, tutorial: { path: string }) => { + const fullPath = `/${group.path}/${tutorial.path}`; + return this.docs.currentPath === fullPath; };