@abreen/tada 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/tada.ts +81 -57
- package/build/apply-base-path-plugin.ts +22 -6
- package/build/code.test.ts +108 -1
- package/build/colors.test.ts +66 -0
- package/build/columns-plugin.ts +116 -0
- package/build/content-watch.ts +30 -21
- package/build/copy.ts +1 -1
- package/build/external-links-plugin.ts +1 -1
- package/build/generate-content-assets.ts +48 -6
- package/build/generate-favicon.ts +1 -1
- package/build/generate-fonts.ts +1 -1
- package/build/generate-katex-assets.ts +1 -1
- package/build/log.test.ts +7 -1
- package/build/log.ts +4 -1
- package/build/markdown-plugins.test.ts +65 -3
- package/build/pagefind.ts +1 -1
- package/build/pipeline.ts +4 -20
- package/build/serve.test.ts +59 -0
- package/build/serve.ts +29 -63
- package/build/templates.ts +2 -2
- package/build/toc-plugin.test.ts +40 -25
- package/build/toc-plugin.ts +9 -2
- package/build/{types.ts → types.d.ts} +88 -1
- package/build/util.ts +1 -1
- package/build/utils/code.ts +76 -3
- package/build/utils/content-files.test.ts +56 -0
- package/build/utils/content-files.ts +15 -11
- package/build/utils/file-types.test.ts +18 -0
- package/build/utils/file-types.ts +4 -0
- package/build/utils/include.test.ts +117 -0
- package/build/utils/include.ts +53 -0
- package/build/utils/jdi-runner/TraceRunner.java +416 -0
- package/build/utils/literate-java.ts +2 -2
- package/build/utils/markdown.ts +32 -14
- package/build/utils/paths.ts +1 -1
- package/build/utils/render.ts +102 -11
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/utils/trace-layout.test.ts +570 -0
- package/build/utils/trace-layout.ts +848 -0
- package/build/utils/trace-svg.test.ts +425 -0
- package/build/utils/trace-svg.ts +905 -0
- package/build/utils/trace.test.ts +169 -0
- package/build/utils/trace.ts +382 -0
- package/build/validate-internal-links-plugin.test.ts +30 -2
- package/build/validate-internal-links-plugin.ts +3 -3
- package/build/watch-reload-client.ts +4 -20
- package/build/watch.ts +436 -448
- package/init/content/index.md +10 -5
- package/init/content/labs/00/index.md +1 -1
- package/init/content/labs/01/SearchTreeDemo.java +31 -0
- package/init/content/labs/01/TraceDemo.java +22 -0
- package/init/content/labs/01/index.md +27 -0
- package/init/content/labs/index.md +2 -0
- package/init/content/lectures/01/Rectangle.java +2 -0
- package/init/content/lectures/01/index.md +2 -1
- package/init/content/lectures/01/rectangle.py +45 -0
- package/init/content/lectures/02/_pr1.md +5 -0
- package/init/content/lectures/02/index.md +11 -0
- package/init/content/lectures/02/subdir/_foobar.html +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +6 -0
- package/init/content/lectures/index.md +3 -0
- package/init/content/markdown.md +30 -0
- package/package.json +21 -4
- package/src/_base.scss +13 -2
- package/src/_layout.scss +6 -5
- package/src/anchor/index.test.ts +64 -0
- package/src/code/index.test.ts +71 -0
- package/src/code/style.scss +23 -0
- package/src/code.scss +5 -3
- package/src/header/_base.scss +48 -9
- package/src/header/index.test.ts +89 -0
- package/src/header/index.ts +1 -1
- package/src/header/style.scss +1 -5
- package/src/index.ts +18 -20
- package/src/literate/style.scss +15 -7
- package/src/navigate/eligible.test.ts +101 -0
- package/src/navigate/eligible.ts +62 -0
- package/src/navigate/index.test.ts +82 -0
- package/src/navigate/index.ts +365 -0
- package/src/navigate/lifecycle.test.ts +26 -0
- package/src/navigate/lifecycle.ts +65 -0
- package/src/navigate/style.scss +61 -0
- package/src/print/index.test.ts +71 -0
- package/src/question/index.test.ts +82 -0
- package/src/question/style.scss +12 -0
- package/src/search/index.ts +38 -14
- package/src/search/style.scss +23 -14
- package/src/style.scss +25 -12
- package/src/timezone/index.test.ts +301 -1
- package/src/timezone/index.ts +68 -56
- package/src/toc/index.test.ts +237 -0
- package/src/toc/index.ts +29 -69
- package/src/toc/style.scss +17 -0
- package/src/top/index.test.ts +46 -0
- package/src/top/index.ts +2 -2
- package/src/trace/index.test.ts +554 -0
- package/src/trace/index.ts +258 -0
- package/src/trace/style.scss +210 -0
- package/src/trace/types.d.ts +47 -0
- package/src/util.test.ts +135 -0
- package/templates/_top.html +23 -22
- package/types/jsdom.d.ts +9 -0
package/src/search/index.ts
CHANGED
|
@@ -104,6 +104,7 @@ function render(
|
|
|
104
104
|
input: HTMLInputElement,
|
|
105
105
|
resultsContainer: HTMLElement,
|
|
106
106
|
state: State,
|
|
107
|
+
loading = false,
|
|
107
108
|
) {
|
|
108
109
|
if (state.showResults) {
|
|
109
110
|
resultsContainer.removeAttribute('inert');
|
|
@@ -201,10 +202,8 @@ function render(
|
|
|
201
202
|
const hint = document.createElement('span');
|
|
202
203
|
hint.className = 'search-hint';
|
|
203
204
|
const kbd1 = document.createElement('kbd');
|
|
204
|
-
kbd1.textContent = '
|
|
205
|
-
|
|
206
|
-
kbd2.textContent = 'Space';
|
|
207
|
-
hint.append(kbd1, '+', kbd2, '\u00a0to search');
|
|
205
|
+
kbd1.textContent = '/';
|
|
206
|
+
hint.append('Press\u00a0', kbd1, '\u00a0to search');
|
|
208
207
|
infoSpan.appendChild(hint);
|
|
209
208
|
|
|
210
209
|
resultsDiv.insertBefore(infoSpan, resultsDiv.firstChild);
|
|
@@ -218,15 +217,21 @@ function render(
|
|
|
218
217
|
countSpan.className = 'results-count';
|
|
219
218
|
infoSpan.insertBefore(countSpan, infoSpan.firstChild);
|
|
220
219
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
} else if (n === 1) {
|
|
225
|
-
countSpan.textContent = 'One result';
|
|
226
|
-
} else if (n <= MAX_RESULTS) {
|
|
227
|
-
countSpan.textContent = `${n} results`;
|
|
220
|
+
if (loading) {
|
|
221
|
+
countSpan.textContent = 'Loading\u2026';
|
|
222
|
+
infoSpan.classList.add('loading');
|
|
228
223
|
} else {
|
|
229
|
-
|
|
224
|
+
infoSpan.classList.remove('loading');
|
|
225
|
+
const n = state.totalResults;
|
|
226
|
+
if (n === 0) {
|
|
227
|
+
countSpan.textContent = 'No results';
|
|
228
|
+
} else if (n === 1) {
|
|
229
|
+
countSpan.textContent = 'One result';
|
|
230
|
+
} else if (n <= MAX_RESULTS) {
|
|
231
|
+
countSpan.textContent = `${n} results`;
|
|
232
|
+
} else {
|
|
233
|
+
countSpan.textContent = `Showing first ${MAX_RESULTS} results`;
|
|
234
|
+
}
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
if (state.showResults) {
|
|
@@ -328,11 +333,20 @@ export default (window: Window) => {
|
|
|
328
333
|
};
|
|
329
334
|
|
|
330
335
|
async function update() {
|
|
336
|
+
if (state.showResults) {
|
|
337
|
+
render(input!, resultsContainer, state, true);
|
|
338
|
+
}
|
|
331
339
|
await doSearch(state);
|
|
332
|
-
|
|
340
|
+
if (!state.showResults) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
render(input!, resultsContainer, state, false);
|
|
333
344
|
}
|
|
334
345
|
|
|
335
346
|
function hide() {
|
|
347
|
+
if (!state.showResults) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
336
350
|
state.showResults = false;
|
|
337
351
|
render(input!, resultsContainer, state);
|
|
338
352
|
}
|
|
@@ -343,6 +357,12 @@ export default (window: Window) => {
|
|
|
343
357
|
return;
|
|
344
358
|
}
|
|
345
359
|
state.value = value;
|
|
360
|
+
if (value.length === 0) {
|
|
361
|
+
hide();
|
|
362
|
+
state.results = [];
|
|
363
|
+
state.totalResults = 0;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
346
366
|
state.showResults = true;
|
|
347
367
|
update().catch(() => {});
|
|
348
368
|
}
|
|
@@ -481,7 +501,11 @@ export default (window: Window) => {
|
|
|
481
501
|
|
|
482
502
|
function handleWindowKeyDown(e: KeyboardEvent) {
|
|
483
503
|
resultsContainer.classList.add('is-typing');
|
|
484
|
-
if (e.key === '
|
|
504
|
+
if (e.key === '/' && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) {
|
|
505
|
+
const tag = (e.target as HTMLElement)?.tagName;
|
|
506
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
485
509
|
e.preventDefault();
|
|
486
510
|
input!.focus();
|
|
487
511
|
}
|
package/src/search/style.scss
CHANGED
|
@@ -14,13 +14,12 @@ header {
|
|
|
14
14
|
|
|
15
15
|
.results-container.is-showing .results {
|
|
16
16
|
opacity: 1;
|
|
17
|
-
transform: translateY(0);
|
|
18
17
|
pointer-events: all;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
@media (hover: hover) {
|
|
22
|
-
.results-container.is-typing a.result:hover:not(:focus
|
|
23
|
-
.results-container.is-typing a.sub-result:hover:not(:focus
|
|
21
|
+
.results-container.is-typing a.result:hover:not(:focus),
|
|
22
|
+
.results-container.is-typing a.sub-result:hover:not(:focus) {
|
|
24
23
|
background: transparent;
|
|
25
24
|
}
|
|
26
25
|
}
|
|
@@ -41,13 +40,6 @@ header {
|
|
|
41
40
|
min-height: 80px;
|
|
42
41
|
@include user-select(none);
|
|
43
42
|
|
|
44
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
45
|
-
transform: translateY(-8px);
|
|
46
|
-
transition:
|
|
47
|
-
opacity 0.25s ease,
|
|
48
|
-
transform 0.25s ease;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
43
|
.results-info {
|
|
52
44
|
display: flex;
|
|
53
45
|
align-items: center;
|
|
@@ -55,6 +47,25 @@ header {
|
|
|
55
47
|
color: var(--fg2-color);
|
|
56
48
|
@include user-select(none);
|
|
57
49
|
padding: 8px 8px 8px 8px;
|
|
50
|
+
position: relative;
|
|
51
|
+
|
|
52
|
+
&.loading::after {
|
|
53
|
+
content: '';
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: 0;
|
|
56
|
+
left: 0;
|
|
57
|
+
right: 0;
|
|
58
|
+
height: 2px;
|
|
59
|
+
background-image: linear-gradient(
|
|
60
|
+
90deg,
|
|
61
|
+
transparent 25%,
|
|
62
|
+
var(--theme-color) 50%,
|
|
63
|
+
transparent 75%
|
|
64
|
+
);
|
|
65
|
+
background-size: 200% 100%;
|
|
66
|
+
background-repeat: no-repeat;
|
|
67
|
+
animation: shimmer 3s ease-in-out infinite;
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
.search-hint {
|
|
60
71
|
font-size: var(--font-size-small);
|
|
@@ -89,9 +100,8 @@ header {
|
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
|
|
92
|
-
a.result:focus
|
|
103
|
+
a.result:focus {
|
|
93
104
|
background: var(--bg2-color);
|
|
94
|
-
outline: 2px solid var(--theme-color);
|
|
95
105
|
outline-offset: -2px;
|
|
96
106
|
}
|
|
97
107
|
}
|
|
@@ -162,9 +172,8 @@ header {
|
|
|
162
172
|
background: var(--bg2-color);
|
|
163
173
|
}
|
|
164
174
|
|
|
165
|
-
&:focus
|
|
175
|
+
&:focus {
|
|
166
176
|
background: var(--bg2-color);
|
|
167
|
-
outline: 2px solid var(--theme-color);
|
|
168
177
|
outline-offset: -2px;
|
|
169
178
|
}
|
|
170
179
|
|
package/src/style.scss
CHANGED
|
@@ -167,27 +167,36 @@ input[type='submit'] {
|
|
|
167
167
|
text-decoration: none;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
button:
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
button:disabled,
|
|
171
|
+
input[type='submit']:disabled {
|
|
172
|
+
opacity: 0.4;
|
|
173
|
+
cursor: not-allowed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
button:active:not(:disabled),
|
|
177
|
+
a.button:not(.disabled):active,
|
|
178
|
+
input[type='submit']:active:not(:disabled) {
|
|
173
179
|
background: var(--link-active-color) !important;
|
|
174
180
|
color: var(--bg-color) !important;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
@media (hover: hover) {
|
|
178
|
-
button:hover,
|
|
179
|
-
a.button:hover,
|
|
180
|
-
input[type='submit']:hover {
|
|
184
|
+
button:hover:not(:disabled),
|
|
185
|
+
a.button:not(.disabled):hover,
|
|
186
|
+
input[type='submit']:hover:not(:disabled) {
|
|
181
187
|
background: var(--bg2-color);
|
|
182
188
|
cursor: pointer;
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
button:focus
|
|
187
|
-
a.button:focus
|
|
188
|
-
input[type='submit']:focus
|
|
189
|
-
input[type='text']:focus
|
|
190
|
-
input[type='search']:focus
|
|
192
|
+
button:focus,
|
|
193
|
+
a.button:focus,
|
|
194
|
+
input[type='submit']:focus,
|
|
195
|
+
input[type='text']:focus,
|
|
196
|
+
input[type='search']:focus,
|
|
197
|
+
select:focus {
|
|
198
|
+
outline: 2px solid var(--theme-color);
|
|
199
|
+
outline-offset: 1px;
|
|
191
200
|
border-color: var(--fg-color);
|
|
192
201
|
background-color: var(--bg2-color);
|
|
193
202
|
}
|
|
@@ -206,6 +215,10 @@ button.icon-button {
|
|
|
206
215
|
vertical-align: top;
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
button.icon-button:focus {
|
|
219
|
+
background-color: var(--bg2-color);
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
ol.styled-list,
|
|
210
223
|
ul.styled-list {
|
|
211
224
|
color: var(--theme-color-text);
|
|
@@ -458,7 +471,7 @@ select {
|
|
|
458
471
|
font-size: inherit;
|
|
459
472
|
}
|
|
460
473
|
|
|
461
|
-
// Shiki dual-theme support (defaultColor: false
|
|
474
|
+
// Shiki dual-theme support (defaultColor: false, CSS custom properties only,
|
|
462
475
|
// no inline color/background-color, so no !important needed)
|
|
463
476
|
.shiki span {
|
|
464
477
|
color: var(--shiki-light);
|
|
@@ -1,5 +1,45 @@
|
|
|
1
|
+
// Set globals before importing the module, since it captures
|
|
2
|
+
// __SITE_TIMEZONES__ at load time.
|
|
3
|
+
const TIMEZONES: TimeZone[] = [
|
|
4
|
+
{ value: 'America/New_York', label: 'US Eastern', abbreviation: 'ET' },
|
|
5
|
+
{ value: 'America/Chicago', label: 'US Central', abbreviation: 'CT' },
|
|
6
|
+
{ value: 'America/Los_Angeles', label: 'US Pacific', abbreviation: 'PT' },
|
|
7
|
+
];
|
|
8
|
+
(globalThis as Record<string, unknown>).__SITE_TIMEZONES__ = TIMEZONES;
|
|
9
|
+
(globalThis as Record<string, unknown>).__SITE_DEFAULT_TIMEZONE__ =
|
|
10
|
+
'America/New_York';
|
|
11
|
+
|
|
1
12
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
13
|
+
import { JSDOM } from 'jsdom';
|
|
14
|
+
import mount, { detectPeriodStyle, to12Hour, normalizeHM } from './index';
|
|
15
|
+
|
|
16
|
+
function dom(bodyHtml: string) {
|
|
17
|
+
return new JSDOM(`<body>${bodyHtml}</body>`, { url: 'http://localhost/' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createPage(times: string[], selectedTz?: string) {
|
|
21
|
+
const options = TIMEZONES.map(
|
|
22
|
+
tz => `<option value="${tz.value}">${tz.label}</option>`,
|
|
23
|
+
).join('');
|
|
24
|
+
const timeEls = times
|
|
25
|
+
.map(t => `<time datetime="${t}">${to12Hour(...parseHM(t))}</time>`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
const html = `<select class="time-zone">${options}</select>${timeEls}`;
|
|
28
|
+
const jsdom = dom(html);
|
|
29
|
+
if (selectedTz) {
|
|
30
|
+
try {
|
|
31
|
+
jsdom.window.localStorage.setItem('timezoneSelection', selectedTz);
|
|
32
|
+
} catch {
|
|
33
|
+
// ignored
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return jsdom.window;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseHM(hhmm: string): [number, number] {
|
|
40
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
41
|
+
return [h, m];
|
|
42
|
+
}
|
|
3
43
|
|
|
4
44
|
describe('detectPeriodStyle', () => {
|
|
5
45
|
test('detects uppercase "PM"', () => {
|
|
@@ -75,6 +115,10 @@ describe('to12Hour', () => {
|
|
|
75
115
|
test('formats 23:59', () => {
|
|
76
116
|
expect(to12Hour(23, 59)).toBe('11:59 p.m.');
|
|
77
117
|
});
|
|
118
|
+
|
|
119
|
+
test('omits period when style is null', () => {
|
|
120
|
+
expect(to12Hour(14, 0, null)).toBe('2:00');
|
|
121
|
+
});
|
|
78
122
|
});
|
|
79
123
|
|
|
80
124
|
describe('normalizeHM', () => {
|
|
@@ -98,3 +142,259 @@ describe('normalizeHM', () => {
|
|
|
98
142
|
expect(normalizeHM(750)).toEqual([12, 30]);
|
|
99
143
|
});
|
|
100
144
|
});
|
|
145
|
+
|
|
146
|
+
describe('timezone mount', () => {
|
|
147
|
+
test('unhides and enables the select element', () => {
|
|
148
|
+
const win = createPage(['09:00']);
|
|
149
|
+
mount(win);
|
|
150
|
+
|
|
151
|
+
const sel = win.document.querySelector(
|
|
152
|
+
'select.time-zone',
|
|
153
|
+
) as HTMLSelectElement;
|
|
154
|
+
expect(sel.hidden).toBe(false);
|
|
155
|
+
expect(sel.disabled).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('sets select value to default timezone', () => {
|
|
159
|
+
const win = createPage(['09:00']);
|
|
160
|
+
mount(win);
|
|
161
|
+
|
|
162
|
+
const sel = win.document.querySelector(
|
|
163
|
+
'select.time-zone',
|
|
164
|
+
) as HTMLSelectElement;
|
|
165
|
+
expect(sel.value).toBe('America/New_York');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('wraps select in a timezone-wrapper div', () => {
|
|
169
|
+
const win = createPage(['09:00']);
|
|
170
|
+
mount(win);
|
|
171
|
+
|
|
172
|
+
const wrapper = win.document.querySelector('.timezone-wrapper');
|
|
173
|
+
expect(wrapper).not.toBeNull();
|
|
174
|
+
expect(wrapper!.querySelector('select.time-zone')).not.toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('adds a reset button inside the wrapper', () => {
|
|
178
|
+
const win = createPage(['09:00']);
|
|
179
|
+
mount(win);
|
|
180
|
+
|
|
181
|
+
const btn = win.document.querySelector('.timezone-wrapper button');
|
|
182
|
+
expect(btn).not.toBeNull();
|
|
183
|
+
expect(btn!.className).toBe('icon-button');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('reset button is initially invisible for default timezone', () => {
|
|
187
|
+
const win = createPage(['09:00']);
|
|
188
|
+
mount(win);
|
|
189
|
+
|
|
190
|
+
const btn = win.document.querySelector(
|
|
191
|
+
'.timezone-wrapper button',
|
|
192
|
+
) as HTMLButtonElement;
|
|
193
|
+
expect(btn.style.opacity).toBe('0');
|
|
194
|
+
expect(btn.style.pointerEvents).toBe('none');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('restores timezone from localStorage', () => {
|
|
198
|
+
const win = createPage(['09:00'], 'America/Chicago');
|
|
199
|
+
mount(win);
|
|
200
|
+
|
|
201
|
+
const sel = win.document.querySelector(
|
|
202
|
+
'select.time-zone',
|
|
203
|
+
) as HTMLSelectElement;
|
|
204
|
+
expect(sel.value).toBe('America/Chicago');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('reset button is visible when non-default timezone is stored', () => {
|
|
208
|
+
const win = createPage(['09:00'], 'America/Chicago');
|
|
209
|
+
mount(win);
|
|
210
|
+
|
|
211
|
+
const btn = win.document.querySelector(
|
|
212
|
+
'.timezone-wrapper button',
|
|
213
|
+
) as HTMLButtonElement;
|
|
214
|
+
expect(btn.style.opacity).toBe('1');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('changing select updates time elements', () => {
|
|
218
|
+
const win = createPage(['12:00']);
|
|
219
|
+
mount(win);
|
|
220
|
+
|
|
221
|
+
const sel = win.document.querySelector(
|
|
222
|
+
'select.time-zone',
|
|
223
|
+
) as HTMLSelectElement;
|
|
224
|
+
const time = win.document.querySelector('time') as HTMLTimeElement;
|
|
225
|
+
|
|
226
|
+
const before = time.innerHTML;
|
|
227
|
+
|
|
228
|
+
sel.value = 'America/Los_Angeles';
|
|
229
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
230
|
+
|
|
231
|
+
// The time text should have changed
|
|
232
|
+
expect(time.innerHTML).not.toBe(before);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('changing to non-default timezone adds is-modified class', () => {
|
|
236
|
+
const win = createPage(['12:00']);
|
|
237
|
+
mount(win);
|
|
238
|
+
|
|
239
|
+
const sel = win.document.querySelector(
|
|
240
|
+
'select.time-zone',
|
|
241
|
+
) as HTMLSelectElement;
|
|
242
|
+
const time = win.document.querySelector('time') as HTMLTimeElement;
|
|
243
|
+
|
|
244
|
+
sel.value = 'America/Los_Angeles';
|
|
245
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
246
|
+
|
|
247
|
+
expect(time.classList.contains('is-modified')).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('changing back to default removes is-modified class', () => {
|
|
251
|
+
const win = createPage(['12:00']);
|
|
252
|
+
mount(win);
|
|
253
|
+
|
|
254
|
+
const sel = win.document.querySelector(
|
|
255
|
+
'select.time-zone',
|
|
256
|
+
) as HTMLSelectElement;
|
|
257
|
+
const time = win.document.querySelector('time') as HTMLTimeElement;
|
|
258
|
+
|
|
259
|
+
sel.value = 'America/Los_Angeles';
|
|
260
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
261
|
+
expect(time.classList.contains('is-modified')).toBe(true);
|
|
262
|
+
|
|
263
|
+
sel.value = 'America/New_York';
|
|
264
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
265
|
+
expect(time.classList.contains('is-modified')).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('reset button restores default timezone', () => {
|
|
269
|
+
const win = createPage(['12:00'], 'America/Chicago');
|
|
270
|
+
mount(win);
|
|
271
|
+
|
|
272
|
+
const sel = win.document.querySelector(
|
|
273
|
+
'select.time-zone',
|
|
274
|
+
) as HTMLSelectElement;
|
|
275
|
+
const btn = win.document.querySelector(
|
|
276
|
+
'.timezone-wrapper button',
|
|
277
|
+
) as HTMLButtonElement;
|
|
278
|
+
|
|
279
|
+
expect(sel.value).toBe('America/Chicago');
|
|
280
|
+
|
|
281
|
+
btn.click();
|
|
282
|
+
|
|
283
|
+
expect(sel.value).toBe('America/New_York');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('reset button removes localStorage entry', () => {
|
|
287
|
+
const win = createPage(['12:00'], 'America/Chicago');
|
|
288
|
+
mount(win);
|
|
289
|
+
|
|
290
|
+
const btn = win.document.querySelector(
|
|
291
|
+
'.timezone-wrapper button',
|
|
292
|
+
) as HTMLButtonElement;
|
|
293
|
+
btn.click();
|
|
294
|
+
|
|
295
|
+
expect(win.localStorage.getItem('timezoneSelection')).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('changing timezone saves to localStorage', () => {
|
|
299
|
+
const win = createPage(['12:00']);
|
|
300
|
+
mount(win);
|
|
301
|
+
|
|
302
|
+
const sel = win.document.querySelector(
|
|
303
|
+
'select.time-zone',
|
|
304
|
+
) as HTMLSelectElement;
|
|
305
|
+
sel.value = 'America/Chicago';
|
|
306
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
307
|
+
|
|
308
|
+
expect(win.localStorage.getItem('timezoneSelection')).toBe(
|
|
309
|
+
'America/Chicago',
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('selecting default timezone removes localStorage entry', () => {
|
|
314
|
+
const win = createPage(['12:00'], 'America/Chicago');
|
|
315
|
+
mount(win);
|
|
316
|
+
|
|
317
|
+
const sel = win.document.querySelector(
|
|
318
|
+
'select.time-zone',
|
|
319
|
+
) as HTMLSelectElement;
|
|
320
|
+
sel.value = 'America/New_York';
|
|
321
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
322
|
+
|
|
323
|
+
expect(win.localStorage.getItem('timezoneSelection')).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('time element gets title with original time on change', () => {
|
|
327
|
+
const win = createPage(['14:00']);
|
|
328
|
+
mount(win);
|
|
329
|
+
|
|
330
|
+
const sel = win.document.querySelector(
|
|
331
|
+
'select.time-zone',
|
|
332
|
+
) as HTMLSelectElement;
|
|
333
|
+
const time = win.document.querySelector('time') as HTMLTimeElement;
|
|
334
|
+
|
|
335
|
+
sel.value = 'America/Chicago';
|
|
336
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
337
|
+
|
|
338
|
+
expect(time.title).toContain('ET');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('time element title is cleared when returning to default', () => {
|
|
342
|
+
const win = createPage(['14:00']);
|
|
343
|
+
mount(win);
|
|
344
|
+
|
|
345
|
+
const sel = win.document.querySelector(
|
|
346
|
+
'select.time-zone',
|
|
347
|
+
) as HTMLSelectElement;
|
|
348
|
+
const time = win.document.querySelector('time') as HTMLTimeElement;
|
|
349
|
+
|
|
350
|
+
sel.value = 'America/Chicago';
|
|
351
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
352
|
+
expect(time.title).not.toBe('');
|
|
353
|
+
|
|
354
|
+
sel.value = 'America/New_York';
|
|
355
|
+
sel.dispatchEvent(new win.Event('change'));
|
|
356
|
+
expect(time.title).toBe('');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('ignores stored timezone not in the timezone list', () => {
|
|
360
|
+
const win = createPage(['09:00']);
|
|
361
|
+
try {
|
|
362
|
+
win.localStorage.setItem('timezoneSelection', 'Mars/Olympus_Mons');
|
|
363
|
+
} catch {
|
|
364
|
+
// ignored
|
|
365
|
+
}
|
|
366
|
+
mount(win);
|
|
367
|
+
|
|
368
|
+
const sel = win.document.querySelector(
|
|
369
|
+
'select.time-zone',
|
|
370
|
+
) as HTMLSelectElement;
|
|
371
|
+
expect(sel.value).toBe('America/New_York');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('returns a cleanup function', () => {
|
|
375
|
+
const win = createPage(['09:00']);
|
|
376
|
+
const cleanup = mount(win);
|
|
377
|
+
expect(typeof cleanup).toBe('function');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('multiple select elements stay in sync', () => {
|
|
381
|
+
const options = TIMEZONES.map(
|
|
382
|
+
tz => `<option value="${tz.value}">${tz.label}</option>`,
|
|
383
|
+
).join('');
|
|
384
|
+
const html =
|
|
385
|
+
`<select class="time-zone">${options}</select>` +
|
|
386
|
+
`<select class="time-zone">${options}</select>` +
|
|
387
|
+
'<time datetime="12:00">12:00 p.m.</time>';
|
|
388
|
+
const win = dom(html).window;
|
|
389
|
+
mount(win);
|
|
390
|
+
|
|
391
|
+
const selects = win.document.querySelectorAll(
|
|
392
|
+
'select.time-zone',
|
|
393
|
+
) as NodeListOf<HTMLSelectElement>;
|
|
394
|
+
|
|
395
|
+
selects[0].value = 'America/Chicago';
|
|
396
|
+
selects[0].dispatchEvent(new win.Event('change'));
|
|
397
|
+
|
|
398
|
+
expect(selects[1].value).toBe('America/Chicago');
|
|
399
|
+
});
|
|
400
|
+
});
|