Skip to content

Commit 006776a

Browse files
Merge pull request NullVoxPopuli#1904 from tcjr/broken-selection
Tutorial navigation selected state
2 parents 075295a + edaa48d commit 006776a

File tree

9 files changed

+224
-46
lines changed

9 files changed

+224
-46
lines changed

apps/tutorial/app/components/selection.gts

+5-3
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export class Selection extends Component {
2929
this.router.transitionTo(event.target.value);
3030
};
3131

32-
isSelected = ({ path }: { path: string }) => {
33-
return this.docs.currentPath === path;
32+
isSelected = (group: { path: string }, tutorial: { path: string }) => {
33+
const fullPath = `/${group.path}/${tutorial.path}`;
34+
35+
return this.docs.currentPath === fullPath;
3436
};
3537

3638
<template>
@@ -67,7 +69,7 @@ export class Selection extends Component {
6769

6870
<option
6971
value="/{{group.path}}/{{tutorial.path}}"
70-
selected={{this.isSelected tutorial}}
72+
selected={{this.isSelected group tutorial}}
7173
>
7274
{{titleize tutorial.name}}
7375
</option>

apps/tutorial/app/config/environment.d.ts

-15
This file was deleted.

apps/tutorial/app/config/environment.js

-22
This file was deleted.
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-expect-error
3+
import { getGlobalConfig } from '@embroider/macros/src/addon/runtime';
4+
5+
interface Config {
6+
isTesting?: boolean;
7+
environment: string;
8+
modulePrefix: string;
9+
podModulePrefix?: string;
10+
locationType: 'history' | 'hash' | 'none' | 'auto';
11+
rootURL: string;
12+
EmberENV?: Record<string, unknown>;
13+
APP: Record<string, unknown> & { rootElement?: string; autoboot?: boolean };
14+
}
15+
16+
const ENV: Config = {
17+
modulePrefix: 'tutorial',
18+
environment: import.meta.env.DEV ? 'development' : 'production',
19+
rootURL: '/',
20+
locationType: 'history',
21+
EmberENV: {},
22+
APP: {},
23+
};
24+
25+
export default ENV;
26+
27+
export function enterTestMode() {
28+
ENV.locationType = 'none';
29+
ENV.APP.rootElement = '#ember-testing';
30+
ENV.APP.autoboot = false;
31+
32+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
33+
const config = getGlobalConfig()['@embroider/macros'];
34+
35+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
36+
if (config) config.isTesting = true;
37+
}

apps/tutorial/tests/helpers/index.js apps/tutorial/tests/helpers/index.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
setupApplicationTest as upstreamSetupApplicationTest,
33
setupRenderingTest as upstreamSetupRenderingTest,
44
setupTest as upstreamSetupTest,
5+
type SetupTestOptions,
56
} from 'ember-qunit';
67

7-
// This file exists to provide wrappers around ember-qunit's / ember-mocha's
8+
// This file exists to provide wrappers around ember-qunit's
89
// test setup functions. This way, you can easily extend the setup that is
910
// needed per test type.
1011

11-
function setupApplicationTest(hooks, options) {
12+
function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) {
1213
upstreamSetupApplicationTest(hooks, options);
1314

1415
// Additional setup for application tests can be done here.
@@ -23,17 +24,17 @@ function setupApplicationTest(hooks, options) {
2324
// This is also a good place to call test setup functions coming
2425
// from other addons:
2526
//
26-
// setupIntl(hooks); // ember-intl
27+
// setupIntl(hooks, 'en-us'); // ember-intl
2728
// setupMirage(hooks); // ember-cli-mirage
2829
}
2930

30-
function setupRenderingTest(hooks, options) {
31+
function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) {
3132
upstreamSetupRenderingTest(hooks, options);
3233

3334
// Additional setup for rendering tests can be done here.
3435
}
3536

36-
function setupTest(hooks, options) {
37+
function setupTest(hooks: NestedHooks, options?: SetupTestOptions) {
3738
upstreamSetupTest(hooks, options);
3839

3940
// Additional setup for unit tests can be done here.

apps/tutorial/tests/helpers/mocks.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Service from '@ember/service';
2+
3+
/**
4+
* Build the structure used by the docs service for mocking.
5+
*/
6+
function makeDocsTree(treeData: [string, string[]][]) {
7+
const pages = [];
8+
9+
for (const [groupName, tutorialNames] of treeData) {
10+
const groupPath = groupName
11+
.toLowerCase()
12+
.replace(/ /g, '-')
13+
.replace(/[^a-z0-9-]/g, '');
14+
15+
const groupPages = [];
16+
17+
for (const tutorialName of tutorialNames) {
18+
const tutorialPath = tutorialName
19+
.toLowerCase()
20+
.replace(/ /g, '-')
21+
.replace(/[^a-z0-9-]/g, '');
22+
23+
const prosePath = `/${groupPath}/${tutorialPath}/prose.md`;
24+
25+
const page = {
26+
path: tutorialPath,
27+
name: tutorialName,
28+
cleanedName: tutorialName.replace(/-/g, ' '),
29+
pages: [
30+
{
31+
path: prosePath,
32+
name: 'prose',
33+
groupName: tutorialName.replace(/-/g, ' '),
34+
cleanedName: 'prose',
35+
},
36+
],
37+
first: prosePath,
38+
};
39+
40+
groupPages.push(page);
41+
}
42+
43+
const group = {
44+
path: groupPath,
45+
name: groupName,
46+
cleanedName: groupName.replace(/-/g, ' '),
47+
pages: groupPages,
48+
first: `/${groupPath}/${groupPages[0]?.path}/prose.md`,
49+
};
50+
51+
pages.push(group);
52+
}
53+
54+
return {
55+
name: 'root',
56+
pages,
57+
path: 'root',
58+
first: `/${pages[0]?.path}/${pages[0]?.pages[0]?.path}/prose.md`,
59+
};
60+
}
61+
62+
export class MockDocsService extends Service {
63+
#groupsTree = {};
64+
#currentPath = '/';
65+
66+
get grouped() {
67+
return this.#groupsTree;
68+
}
69+
70+
get currentPath() {
71+
return this.#currentPath;
72+
}
73+
74+
// Test extension
75+
_setGroupsData(treeData: [string, string[]][]) {
76+
this.#groupsTree = makeDocsTree(treeData);
77+
}
78+
79+
_setCurrentPath(path: string) {
80+
this.#currentPath = path;
81+
}
82+
}
83+
84+
export class MockRouterService extends Service {
85+
transitionTo(newRoute: string) {
86+
this._assert.step(`transition to: ${newRoute}`);
87+
}
88+
89+
// Test extension
90+
_assert!: Assert;
91+
_setAssert(assert: Assert) {
92+
this._assert = assert;
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { render, select } from '@ember/test-helpers';
2+
import { module, test } from 'qunit';
3+
4+
import { Selection } from 'tutorial/components/selection';
5+
import { setupRenderingTest } from 'tutorial/tests/helpers';
6+
7+
import { MockDocsService, MockRouterService } from '../../helpers/mocks';
8+
9+
module('Integration | Component | selection', function (hooks) {
10+
setupRenderingTest(hooks);
11+
12+
hooks.beforeEach(function () {
13+
this.owner.register('service:docs', MockDocsService);
14+
this.owner.register('service:router', MockRouterService);
15+
});
16+
17+
test('it renders groups and options', async function (assert) {
18+
const docsSvc = this.owner.lookup('service:docs') as unknown as MockDocsService;
19+
20+
docsSvc._setGroupsData([
21+
[
22+
'1-introduction',
23+
['1-basics', '2-adding-data', '3-transforming-data', '4-multiple-transforms'],
24+
],
25+
['2-reactivity', ['1-values', '2-decorated-values', '3-derived-values']],
26+
['3-event-handling', ['1-dom-events']],
27+
]);
28+
29+
await render(<template><Selection /></template>);
30+
assert.dom('select').hasAttribute('name', 'tutorial');
31+
assert.dom('select > optgroup').exists({ count: 3 });
32+
assert.dom('option').exists({ count: 8 });
33+
// Check group labels
34+
assert.dom('optgroup:nth-child(1)').hasAttribute('label', 'Introduction');
35+
assert.dom('optgroup:nth-child(2)').hasAttribute('label', 'Reactivity');
36+
assert.dom('optgroup:nth-child(3)').hasAttribute('label', 'Event Handling');
37+
// Spot check some option values
38+
assert
39+
.dom('optgroup:nth-child(1) option:nth-child(4)')
40+
.hasValue('/1-introduction/4-multiple-transforms');
41+
assert.dom('optgroup:nth-child(1) option:nth-child(4)').hasText('Multiple Transforms');
42+
assert
43+
.dom('optgroup:nth-child(2) option:nth-child(2)')
44+
.hasValue('/2-reactivity/2-decorated-values');
45+
assert.dom('optgroup:nth-child(2) option:nth-child(2)').hasText('Decorated Values');
46+
});
47+
48+
test('it handles selection', async function (assert) {
49+
const routerSvc = this.owner.lookup('service:router') as unknown as MockRouterService;
50+
51+
routerSvc._setAssert(assert);
52+
53+
const docsSvc = this.owner.lookup('service:docs') as unknown as MockDocsService;
54+
55+
docsSvc._setGroupsData([
56+
['english', ['one', 'two', 'three']],
57+
['spanish', ['uno', 'dos', 'tres']],
58+
['german', ['eins', 'zwei', 'drei']],
59+
]);
60+
docsSvc._setCurrentPath('/spanish/dos');
61+
62+
await render(<template><Selection /></template>);
63+
assert.dom('select').hasAttribute('name', 'tutorial');
64+
assert.dom('select > optgroup').exists({ count: 3 });
65+
assert.dom('option').exists({ count: 9 });
66+
67+
assert.dom('select').hasValue('/spanish/dos');
68+
// Not sure why, but `:checked` is how you get selected option
69+
assert.dom('option:checked').hasValue('/spanish/dos').hasText('Dos');
70+
71+
await select('select', '/german/eins');
72+
docsSvc._setCurrentPath('/german/eins');
73+
assert.dom('option:checked').hasValue('/german/eins').hasText('Eins');
74+
75+
await select('select', '/english/three');
76+
docsSvc._setCurrentPath('/english/three');
77+
assert.dom('option:checked').hasValue('/english/three').hasText('Three');
78+
79+
assert.verifySteps(['transition to: /german/eins', 'transition to: /english/three']);
80+
});
81+
});
File renamed without changes.

apps/tutorial/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"tutorial/*": ["./app/*"],
1515
"*": ["./types/*"]
1616
},
17-
"types": ["ember-source/types", "@embroider/core/virtual"]
17+
"types": ["vite/client", "ember-source/types", "@embroider/core/virtual"]
1818
},
1919
"include": ["app/**/*", "tests/**/*", "types/**/*"]
2020
}

0 commit comments

Comments
 (0)