Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tutorial navigation selected state #1904

Merged
merged 3 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/tutorial/app/components/selection.gts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ 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;
};

<template>
Expand Down Expand Up @@ -67,7 +69,7 @@ export class Selection extends Component {

<option
value="/{{group.path}}/{{tutorial.path}}"
selected={{this.isSelected tutorial}}
selected={{this.isSelected group tutorial}}
>
{{titleize tutorial.name}}
</option>
Expand Down
15 changes: 0 additions & 15 deletions apps/tutorial/app/config/environment.d.ts

This file was deleted.

22 changes: 0 additions & 22 deletions apps/tutorial/app/config/environment.js

This file was deleted.

37 changes: 37 additions & 0 deletions apps/tutorial/app/config/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { getGlobalConfig } from '@embroider/macros/src/addon/runtime';

interface Config {
isTesting?: boolean;
environment: string;
modulePrefix: string;
podModulePrefix?: string;
locationType: 'history' | 'hash' | 'none' | 'auto';
rootURL: string;
EmberENV?: Record<string, unknown>;
APP: Record<string, unknown> & { rootElement?: string; autoboot?: boolean };
}

const ENV: Config = {
modulePrefix: 'tutorial',
environment: import.meta.env.DEV ? 'development' : 'production',
rootURL: '/',
locationType: 'history',
EmberENV: {},
APP: {},
};

export default ENV;

export function enterTestMode() {
ENV.locationType = 'none';
ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const config = getGlobalConfig()['@embroider/macros'];

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (config) config.isTesting = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
94 changes: 94 additions & 0 deletions apps/tutorial/tests/helpers/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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;
}
}
81 changes: 81 additions & 0 deletions apps/tutorial/tests/integration/components/selection-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, select } from '@ember/test-helpers';
import { module, test } from 'qunit';

import { Selection } from 'tutorial/components/selection';
import { setupRenderingTest } from 'tutorial/tests/helpers';

import { MockDocsService, MockRouterService } from '../../helpers/mocks';

module('Integration | Component | selection', function (hooks) {
setupRenderingTest(hooks);

hooks.beforeEach(function () {
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(<template><Selection /></template>);
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(<template><Selection /></template>);
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']);
});
});
File renamed without changes.
2 changes: 1 addition & 1 deletion apps/tutorial/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"tutorial/*": ["./app/*"],
"*": ["./types/*"]
},
"types": ["ember-source/types", "@embroider/core/virtual"]
"types": ["vite/client", "ember-source/types", "@embroider/core/virtual"]
},
"include": ["app/**/*", "tests/**/*", "types/**/*"]
}
Loading