Repository navigation
Expand file tree
/
Copy pathe2e-i18n.mjs
More file actions
179 lines (163 loc) · 9.35 KB
/
Copy pathe2e-i18n.mjs
File metadata and controls
179 lines (163 loc) · 9.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// UI translations e2e. Covers: English default; auto-detection from the browser language (a de-DE context
// renders German with zero configuration).
// SAFE with a live config: creates only dashboard:nh-e2e-i18n-a/-b/-c (clock/label/selection/ image/frame
// widgets; the selection reads one item and commands nothing), exact-uid cleanup.
import { launchChromium } from './lib/browser.mjs'
import { BASE, APP, NS, TOKEN, AUTH, ITEMS } from './lib/target.mjs'
const results = []
const ok = (name, cond, detail = '') => {
results.push({ name, pass: !!cond })
console.log((cond ? 'PASS ' : 'FAIL ') + name + (detail ? ' [' + detail + ']' : ''))
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function launch() {
for (const channel of ['chrome', 'msedge']) {
try { return await launchChromium({ channel, headless: true }) } catch {}
}
return launchChromium({ headless: true })
}
const dash = (id, name, widgets) => ({
uid: 'dashboard:' + id,
component: 'neohab:dashboard',
config: { version: 1, id, name, columns: 12, rowHeight: 'match', gap: 5, widgets },
})
const seed = async (comp) => {
await fetch(NS + '/' + comp.uid, { method: 'DELETE', headers: AUTH }).catch(() => {})
const r = await fetch(NS, { method: 'POST', headers: { ...AUTH, 'Content-Type': 'application/json' }, body: JSON.stringify(comp) })
return r.ok
}
const UIDS = ['dashboard:nh-e2e-i18n-a', 'dashboard:nh-e2e-i18n-b', 'dashboard:nh-e2e-i18n-c']
const browser = await launch()
try {
ok('seed a (1 widget)', await seed(dash('nh-e2e-i18n-a', 'E2E I18N A', [
{ id: 'w1', type: 'clock', config: {}, layout: { lg: { x: 0, y: 0, w: 3, h: 2 } } },
])))
ok('seed b (3 widgets)', await seed(dash('nh-e2e-i18n-b', 'E2E I18N B', [
{ id: 'w1', type: 'clock', config: {}, layout: { lg: { x: 0, y: 0, w: 3, h: 2 } } },
{ id: 'w2', type: 'label', config: { text: 'mein eigener text' }, layout: { lg: { x: 3, y: 0, w: 3, h: 2 } } },
{ id: 'w3', type: 'label', config: { text: 'zzz' }, layout: { lg: { x: 6, y: 0, w: 3, h: 2 } } },
])))
ok('seed c (unconfigured widgets)', await seed(dash('nh-e2e-i18n-c', 'E2E I18N C', [
// bound but with no choices: with no item at all the host says "no item yet" instead, which is a
// different string and would leave the selection's own one untested
{ id: 'w1', type: 'selection', config: { label: 'Sel', item: ITEMS.switch ?? ITEMS.dimmer, choices: '' }, layout: { lg: { x: 0, y: 0, w: 4, h: 3 } } },
{ id: 'w2', type: 'image', config: { label: 'Img', url: '' }, layout: { lg: { x: 4, y: 0, w: 4, h: 3 } } },
{ id: 'w3', type: 'frame', config: { label: 'Frm', url: '' }, layout: { lg: { x: 8, y: 0, w: 4, h: 3 } } },
])))
const openCtx = async (locale, withToken = true) => {
const ctx = await browser.newContext({ viewport: { width: 1400, height: 950 }, locale })
if (withToken) {
await ctx.addInitScript((t) => { try { localStorage.setItem('neohab:apiToken', t) } catch {} }, TOKEN)
}
const page = await ctx.newPage()
const track = { errs: [], catalogRequests: [] }
page.on('pageerror', (e) => track.errs.push(String(e.message)))
page.on('console', (m) => m.type() === 'error' && track.errs.push(m.text()))
page.on('request', (r) => {
const m = /assets\/((?:de|es|fr|it|nl|pl)-[\w-]+\.js)$/.exec(r.url())
if (m) track.catalogRequests.push(m[1])
})
return { ctx, page, track }
}
{
const { ctx, page, track } = await openCtx('en-US')
await page.goto(APP + '#/settings', { waitUntil: 'domcontentloaded' })
await page.waitForSelector('.nh-theme__pick', { timeout: 20000 })
ok('en: Appearance heading', (await page.locator('h2:text-is("Appearance")').count()) === 1)
ok('en: language field defaults to Auto', (await page.locator('#nh-set-lang').inputValue()) === 'auto')
await sleep(500)
ok('en: no language chunk fetched at all', track.catalogRequests.length === 0, track.catalogRequests.join(','))
ok('en: html lang', await page.evaluate(() => document.documentElement.lang) === 'en')
await ctx.close()
}
{
const { ctx, page, track } = await openCtx('de-DE')
await page.goto(APP + '#/settings', { waitUntil: 'domcontentloaded' })
await page.waitForSelector('h2:text-is("Erscheinungsbild")', { timeout: 20000 })
ok('de-auto: Appearance renders as Erscheinungsbild', true)
ok('de-auto: exactly the German chunk fetched', track.catalogRequests.length === 1 && track.catalogRequests[0].startsWith('de-'), track.catalogRequests.join(','))
ok('de-auto: html lang', await page.evaluate(() => document.documentElement.lang) === 'de')
await page.goto(APP + '#/')
await page.waitForSelector('.nh-tile', { timeout: 20000 })
const metaA = await page.locator('.nh-tile:has(.nh-tile__name:text-is("E2E I18N A")) .nh-tile__meta').textContent()
const metaB = await page.locator('.nh-tile:has(.nh-tile__name:text-is("E2E I18N B")) .nh-tile__meta').textContent()
ok('de: singular tile meta', metaA === '1 Widget', String(metaA))
ok('de: plural tile meta', metaB === '3 Widgets', String(metaB))
await page.goto(APP + '#/d/nh-e2e-i18n-c')
await page.waitForSelector('.nh-widget', { timeout: 20000 })
// the selection says it is loading until the item's own options have arrived, then that it has none
await page
.waitForFunction(() => (document.querySelector('.nh-selection__empty')?.textContent ?? '').includes('Auswahl'), undefined, { timeout: 15000 })
.catch(() => {})
const emptyStates = await page.evaluate(() => ({
selection: document.querySelector('.nh-selection__empty')?.textContent ?? '',
placeholders: [...document.querySelectorAll('.nh-image__placeholder')].map((e) => e.textContent ?? ''),
}))
ok(
'de: the selection widget says so in German',
emptyStates.selection.includes('Auswahlmöglichkeiten'),
emptyStates.selection
)
ok(
'de: the image widget says so in German',
emptyStates.placeholders.some((p) => p.includes('Keine Bild-URL')),
JSON.stringify(emptyStates.placeholders)
)
ok(
'de: the frame widget says so in German',
emptyStates.placeholders.some((p) => p.includes('Keine URL konfiguriert')),
JSON.stringify(emptyStates.placeholders)
)
ok(
'de: no English empty-state text is left on the screen',
!/No choices|No image URL|No URL configured/.test(
emptyStates.selection + emptyStates.placeholders.join(' ')
),
emptyStates.selection + ' | ' + emptyStates.placeholders.join(' | ')
)
await page.goto(APP + '#/d/nh-e2e-i18n-b')
await page.waitForSelector('.nh-widget', { timeout: 20000 })
ok('de: user widget text untouched', (await page.locator('.nh-label:has-text("mein eigener text")').count()) === 1)
await page.click('[aria-label="Dashboard bearbeiten"]')
await page.waitForSelector('.nh-grid--edit .nh-cell', { timeout: 15000 })
ok('de: edit toolbar Save button is Speichern', (await page.locator('button:has-text("Speichern")').count()) === 1)
await page.locator('.nh-cell').first().locator('.nh-cell__overlay').click()
await page.waitForSelector('.nh-sheet', { timeout: 10000 })
ok('de: clock schema label translated', (await page.locator('.nh-sheet .nh-field__label:text-is("12-Stunden-Anzeige")').count()) === 1)
ok('de: sheet title uses translated widget name', /Uhr/.test((await page.locator('.nh-sheet__title').textContent()) ?? ''))
page.on('dialog', (d) => d.accept())
await page.click('button:has-text("Beenden")')
await page.waitForSelector('[aria-label="Dashboard bearbeiten"]', { timeout: 10000 })
ok('de: console clean', track.errs.length === 0, track.errs.slice(0, 3).join(' | '))
await ctx.close()
}
{
const { ctx, page, track } = await openCtx('en-US')
await page.goto(APP + '#/settings', { waitUntil: 'domcontentloaded' })
await page.waitForSelector('#nh-set-lang', { timeout: 20000 })
await page.selectOption('#nh-set-lang', 'fr')
await page.waitForSelector('h2:text-is("Apparence")', { timeout: 15000 })
ok('pick: French applies live, no reload', true)
ok('pick: French chunk fetched on demand', track.catalogRequests.some((c) => c.startsWith('fr-')), track.catalogRequests.join(','))
await page.reload({ waitUntil: 'domcontentloaded' })
await page.waitForSelector('h2:text-is("Apparence")', { timeout: 20000 })
ok('pick: French persists across a reload', true)
ok('pick: stored choice shown in the selector', (await page.locator('#nh-set-lang').inputValue()) === 'fr')
await page.selectOption('#nh-set-lang', 'auto')
await page.waitForSelector('h2:text-is("Appearance")', { timeout: 15000 })
ok('pick: Auto returns to the browser language', true)
ok('pick: console clean', track.errs.length === 0, track.errs.slice(0, 3).join(' | '))
await ctx.close()
}
} finally {
for (const uid of UIDS) {
await fetch(NS + '/' + encodeURIComponent(uid), { method: 'DELETE', headers: AUTH }).catch(() => {})
}
const left = (await (await fetch(NS, { headers: AUTH })).json()).filter((c) => c.uid.includes('nh-e2e-i18n'))
ok('cleanup: no leftovers', left.length === 0, JSON.stringify(left.map((c) => c.uid)))
await browser.close()
const fails = results.filter((r) => !r.pass)
console.log(`\n${results.length - fails.length}/${results.length} checks passed`)
console.log(fails.length ? 'FAILURES: ' + fails.map((f) => f.name).join(' ; ') : 'ALL PASS')
process.exitCode = fails.length ? 1 : 0
}