@museumwnf/viewer-layout 3.2.2 → 3.4.0
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 +167 -12
- package/dist/ItemDetailView-I__hm63H.js +298 -0
- package/dist/SiteShell-4vV2jIzf.js +192 -0
- package/dist/{Theme-Dd8G1RA3.js → Theme-B5J0XRe1.js} +3 -3
- package/dist/{TimelineResultsView-5Cy-OkZY.js → TimelineResultsView-ChkBn27W.js} +489 -485
- package/dist/chrome-DPuPeWzV.js +1141 -0
- package/dist/components.js +1 -1
- package/dist/data-CJlLA-Mf.js +38 -0
- package/dist/data-DTbAYdr2.js +40 -0
- package/dist/dxa.js +619 -502
- package/dist/index.js +6 -6
- package/dist/{themeSpecs-C_uGarTs.js → themeSpecs-B885l456.js} +3 -2
- package/dist/viewer-layout.css +1 -1
- package/dist/views.js +1 -1
- package/package.json +8 -2
- package/src/dxa/testing.js +857 -0
- package/src/styles/dxa-exhibition.css +389 -0
- package/src/styles/dxa-gallery.css +589 -0
- package/src/tokens.reference.css +25 -7
- package/dist/ItemDetailView-C-X-LgJU.js +0 -284
- package/dist/PageShell-CbJ9lQoQ.js +0 -961
- package/dist/SiteShell-C3OgZO8p.js +0 -154
- package/dist/data-BfyYB8ff.js +0 -33
- package/dist/data-G8uXeEfE.js +0 -35
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
// The DXA family's smoke tests, written once (`@museumwnf/viewer-layout/dxa/testing`).
|
|
2
|
+
//
|
|
3
|
+
// Every gallery and every exhibition renders the same family pages
|
|
4
|
+
// (`galleryConfig`/`exhibitionConfig`), so they share one smoke test. A site's
|
|
5
|
+
// own tests/smoke.test.js calls the family's suite with what only it knows:
|
|
6
|
+
// its configuration, its texts and, for a gallery, the records of its own
|
|
7
|
+
// dataset the tests look for. A site may add tests of its own after the call.
|
|
8
|
+
//
|
|
9
|
+
// The suites run inside the site's own Vitest, which processes this package
|
|
10
|
+
// (viewer-core's `defineViewerConfig` inlines it): `vitest` and the data
|
|
11
|
+
// package resolve from the site.
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
13
|
+
import { loadEntities, mergeMessages } from '@museumwnf/viewer-core'
|
|
14
|
+
import {
|
|
15
|
+
checkOfferedLanguages, checkRoutes, checkSectionMeta, checkTextsRendered, mountSite as mountOn,
|
|
16
|
+
} from '@museumwnf/viewer-core/testing'
|
|
17
|
+
|
|
18
|
+
const WAIT = { timeout: 20000 }
|
|
19
|
+
|
|
20
|
+
// The first word of a record's internal name: a term the keyword search is
|
|
21
|
+
// sure to find, whatever Markdown or punctuation the name carries.
|
|
22
|
+
const firstWord = (name) => name.match(/[\p{L}\p{N}]+/u)[0]
|
|
23
|
+
|
|
24
|
+
function mounter(config, sharedTexts, ownTexts) {
|
|
25
|
+
// The same two layers main.js assembles, in the same order: the shared
|
|
26
|
+
// bundle first, the site's own file last. Mounting without them would prove
|
|
27
|
+
// nothing about the chrome — every text would render as its own name.
|
|
28
|
+
const messages = mergeMessages(sharedTexts, { en: ownTexts })
|
|
29
|
+
return { messages, mountSite: (hash = '#/') => mountOn(config, messages, hash) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The page's text nodes joined by spaces, for `checkTextsRendered`: a raw
|
|
33
|
+
// entry name sits inside one text node, while the host's `textContent` glues
|
|
34
|
+
// neighbouring blocks together ("… online exhibition." + "The Hijaz
|
|
35
|
+
// Railway" reads as an "exhibition.T…" name).
|
|
36
|
+
function spacedText(host) {
|
|
37
|
+
const walker = host.ownerDocument.createTreeWalker(host, 4 /* NodeFilter.SHOW_TEXT */)
|
|
38
|
+
const parts = []
|
|
39
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) parts.push(node.nodeValue)
|
|
40
|
+
return { textContent: parts.join(' ') }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The footer is legacy's: the portal links, the 2004–<year> line and the
|
|
44
|
+
// social-network icons, and no rights-holder line, which legacy never printed
|
|
45
|
+
// (inventory-app#2233).
|
|
46
|
+
function rightsTest(mountSite, manifest, messages) {
|
|
47
|
+
it("renders legacy's footer: the portal links, the copyright line and the social links, no attribution", async () => {
|
|
48
|
+
const { app, host } = await mountSite()
|
|
49
|
+
expect(host.querySelector('.mwnf-footer__attribution')).toBeNull()
|
|
50
|
+
expect(host.querySelector('.mwnf-footer__text').textContent)
|
|
51
|
+
.toContain(`${messages.en['core.footer.copyright']} 2004–`)
|
|
52
|
+
expect(host.querySelectorAll('.mwnf-footer__link').length).toBeGreaterThan(0)
|
|
53
|
+
expect(host.querySelectorAll('.mwnf-footer__social-link')).toHaveLength(6)
|
|
54
|
+
app.unmount()
|
|
55
|
+
}, 20000)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The collection results: the tiles, the "filter further by" column, the
|
|
59
|
+
// summary and the page links. A site may pin its own counts.
|
|
60
|
+
function collectionTest(mountSite, collection = {}) {
|
|
61
|
+
it('renders the collection results on the composed results view', async () => {
|
|
62
|
+
const { app, host } = await mountSite('#/collection-results')
|
|
63
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull(), WAIT)
|
|
64
|
+
expect(host.querySelector('.mwnf-catalogue')).not.toBeNull()
|
|
65
|
+
expect(host.querySelector('.mwnf-catalogue__aside .mwnf-filter')).not.toBeNull()
|
|
66
|
+
expect(host.querySelector('.mwnf-summary__count')).not.toBeNull()
|
|
67
|
+
const tiles = host.querySelectorAll('.mwnf-grid__tile').length
|
|
68
|
+
const paginations = host.querySelectorAll('.mwnf-pagination').length
|
|
69
|
+
if (collection.tiles) expect(tiles).toBe(collection.tiles)
|
|
70
|
+
else expect(tiles).toBeGreaterThan(0)
|
|
71
|
+
if (collection.paginations) expect(paginations).toBe(collection.paginations)
|
|
72
|
+
else expect(paginations).toBeGreaterThan(0)
|
|
73
|
+
app.unmount()
|
|
74
|
+
}, 60000)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function searchTests(mountSite, loadItems) {
|
|
78
|
+
it('returns every renderable object for the all-objects sentinel', async () => {
|
|
79
|
+
const { app, host } = await mountSite('#/search?q=all-objects')
|
|
80
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull(), WAIT)
|
|
81
|
+
expect(host.textContent).toContain('All objects')
|
|
82
|
+
app.unmount()
|
|
83
|
+
}, 30000)
|
|
84
|
+
|
|
85
|
+
it('renders a keyword search on the composed results view', async () => {
|
|
86
|
+
const item = await loadItems()
|
|
87
|
+
const term = firstWord(item.internal_name)
|
|
88
|
+
const { app, host } = await mountSite(`#/search?q=${encodeURIComponent(term)}`)
|
|
89
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull(), WAIT)
|
|
90
|
+
expect(host.textContent).toContain(`“${term}”`)
|
|
91
|
+
app.unmount()
|
|
92
|
+
}, 30000)
|
|
93
|
+
|
|
94
|
+
it('offers the two ways out of an empty keyword search', async () => {
|
|
95
|
+
const { app, host } = await mountSite('#/search?q=zzz-nonexistent-keyword-zzz')
|
|
96
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-catalogue')).not.toBeNull(), WAIT)
|
|
97
|
+
expect(host.textContent).toContain('No items match your search.')
|
|
98
|
+
expect(host.querySelector('a[href="#/how-to-search"]')).not.toBeNull()
|
|
99
|
+
expect(host.querySelector('a[href="#/collection"]')).not.toBeNull()
|
|
100
|
+
app.unmount()
|
|
101
|
+
}, 30000)
|
|
102
|
+
|
|
103
|
+
it('renders the search how-to essay on the composed text page view', async () => {
|
|
104
|
+
const { app, host } = await mountSite('#/how-to-search')
|
|
105
|
+
await vi.waitFor(() => expect(host.textContent).toContain('Boolean Full Text Search'), WAIT)
|
|
106
|
+
expect(host.querySelector('a[href="#/collection"]')).not.toBeNull()
|
|
107
|
+
app.unmount()
|
|
108
|
+
}, 30000)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A record related to an item but outside this site's own package is listed
|
|
112
|
+
// with its project, not as a bare legacy code. Nothing to check in a package
|
|
113
|
+
// whose related records are all its own.
|
|
114
|
+
function outsideReferenceTest(mountSite, manifest, sharedTexts) {
|
|
115
|
+
it('names the project of a related record outside this site', async () => {
|
|
116
|
+
const [items] = await loadEntities(['items'])
|
|
117
|
+
const item = items.find((i) => (i.related_items ?? []).some((ref) => ref.in_package === false))
|
|
118
|
+
if (!item) return
|
|
119
|
+
const projects = item.related_items
|
|
120
|
+
.filter((ref) => ref.in_package === false)
|
|
121
|
+
.map((ref) => manifest.projects[ref.project_id]?.name?.en)
|
|
122
|
+
const notHere = sharedTexts.en['gallery.results.notInThisGallery']
|
|
123
|
+
const { app, host } = await mountSite(`#/item/${item.id}`)
|
|
124
|
+
const outsideRow = () => [...host.querySelectorAll('.mwnf-sheet-related__references li')]
|
|
125
|
+
.find((li) => li.textContent.includes(notHere))
|
|
126
|
+
await vi.waitFor(() => expect(outsideRow()).toBeTruthy(), WAIT)
|
|
127
|
+
const row = outsideRow()
|
|
128
|
+
expect(row.querySelector('.mwnf-chip')).not.toBeNull()
|
|
129
|
+
expect(projects).toContain(row.textContent.replace(notHere, '').trim())
|
|
130
|
+
expect(row.querySelector('code')).toBeNull()
|
|
131
|
+
app.unmount()
|
|
132
|
+
}, 60000)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function configTests(config, routes) {
|
|
136
|
+
it('declares every canonical route by name, and every legacy shape as a redirect', () => {
|
|
137
|
+
expect(checkRoutes(config, routes)).toEqual([])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Every route needs a section for the menu to know where it is, which the
|
|
141
|
+
// config-driven `SiteShell` also relies on for the active menu entry and the
|
|
142
|
+
// banner-title fallback.
|
|
143
|
+
it('gives every route a section', () => {
|
|
144
|
+
expect(checkSectionMeta(config)).toEqual([])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('offers the languages the package declares for the site, where the items carry them', () => {
|
|
148
|
+
expect(checkOfferedLanguages(config)).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reads nothing but the manifest before it mounts', () => {
|
|
152
|
+
expect(config.media.legacyHost).toMatch(/^https:/)
|
|
153
|
+
expect(Object.keys(config.links)).toEqual(
|
|
154
|
+
expect.arrayContaining(['portal', 'galleries', 'myCollection', 'about', 'contact', 'legalNotice', 'credits', 'cookies']),
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The smoke test every DXA gallery runs.
|
|
161
|
+
*
|
|
162
|
+
* - `config`, `sharedTexts` (viewer-i18n's gallery catalogues), `ownTexts`
|
|
163
|
+
* (locales/en.json), `manifest` (the data package's) and `namespace` (this
|
|
164
|
+
* gallery's own text namespace, package.json's `viewerI18n.namespace`).
|
|
165
|
+
* - `picks`: records of this gallery's own dataset, each named in the test
|
|
166
|
+
* that uses it:
|
|
167
|
+
* - `chip`: `{ item, project, className }` — an item borrowed from another
|
|
168
|
+
* project, whose project has a related-database address, with that
|
|
169
|
+
* project's name and its `projectColors` class;
|
|
170
|
+
* - `noticeItem`: an item of Explore Islamic Art Collections, or `null`
|
|
171
|
+
* when the gallery borrows none;
|
|
172
|
+
* - `dynasty`: `{ item, name }` — an item whose dynasty has a translated
|
|
173
|
+
* history, or `null` when no dynasty has one;
|
|
174
|
+
* - `timeline`: `{ code, id, country }` — a country this gallery has dated
|
|
175
|
+
* items in: its legacy two-letter code, its inventory id, its name; and,
|
|
176
|
+
* optionally, the exact `rows` the results' first page shows, the events
|
|
177
|
+
* `found` when that is more, an `event` text, the `gallery` count the
|
|
178
|
+
* results offer, the `galleryTiles` the gallery's first page shows and a
|
|
179
|
+
* `galleryItem` text on it;
|
|
180
|
+
* - `partner`: `{ id, name, city, country, objects }` — a partner with
|
|
181
|
+
* objects, a position and a translated city — and `tiles`, its objects
|
|
182
|
+
* page's first page, when that is fewer than `objects`;
|
|
183
|
+
* - optionally `collection: { tiles, paginations }`, and the `about` and
|
|
184
|
+
* `credits` texts their pages show.
|
|
185
|
+
*/
|
|
186
|
+
export function describeGallerySmoke({ config, sharedTexts, ownTexts, manifest, namespace, picks }) {
|
|
187
|
+
const { messages, mountSite } = mounter(config, sharedTexts, ownTexts)
|
|
188
|
+
const { chip, noticeItem = null, dynasty = null, timeline, partner } = picks
|
|
189
|
+
|
|
190
|
+
describe('gallery smoke test', () => {
|
|
191
|
+
it('mounts against the configured data package', async () => {
|
|
192
|
+
const { app, host } = await mountSite()
|
|
193
|
+
// The home banner's title is legacy's: the gallery's own project name
|
|
194
|
+
// where the gallery is one ("Discover Carpet Art"), its name elsewhere.
|
|
195
|
+
const title = manifest.projects?.[manifest.gallery?.project_id]?.name?.en ?? config.siteName
|
|
196
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-banner__title')?.textContent ?? '').toContain(title), WAIT)
|
|
197
|
+
expect(host.querySelector('.mwnf-page')).not.toBeNull()
|
|
198
|
+
// The family's Home view replaces viewer-core's generic one.
|
|
199
|
+
expect(host.querySelector('.vc-home')).toBeNull()
|
|
200
|
+
app.unmount()
|
|
201
|
+
}, 20000)
|
|
202
|
+
|
|
203
|
+
collectionTest(mountSite, picks.collection)
|
|
204
|
+
|
|
205
|
+
it('renders the collection entrance on the composed search form view', async () => {
|
|
206
|
+
const { app, host } = await mountSite('#/collection')
|
|
207
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-search-form')).not.toBeNull(), WAIT)
|
|
208
|
+
// The country dropdown plus at least one populated tag category, and
|
|
209
|
+
// the shared from/to year buckets.
|
|
210
|
+
expect(host.querySelectorAll('.mwnf-search-form .mwnf-facet').length).toBeGreaterThan(1)
|
|
211
|
+
expect(host.querySelector('.mwnf-search-form__dates')).not.toBeNull()
|
|
212
|
+
expect(host.textContent).toContain('this Gallery’s database')
|
|
213
|
+
app.unmount()
|
|
214
|
+
}, 30000)
|
|
215
|
+
|
|
216
|
+
searchTests(mountSite, async () => (await loadEntities(['items']))[0][0])
|
|
217
|
+
|
|
218
|
+
it('renders the about page on the composed text page view', async () => {
|
|
219
|
+
const { app, host } = await mountSite('#/about')
|
|
220
|
+
const prose = host.querySelector('.mwnf-prose')
|
|
221
|
+
expect(prose.textContent.trim().length).toBeGreaterThan(0)
|
|
222
|
+
if (picks.about) expect(host.textContent).toContain(picks.about)
|
|
223
|
+
app.unmount()
|
|
224
|
+
}, 20000)
|
|
225
|
+
|
|
226
|
+
it('renders the credits page on the composed text page view', async () => {
|
|
227
|
+
const { app, host } = await mountSite('#/credits')
|
|
228
|
+
const prose = host.querySelector('.mwnf-prose')
|
|
229
|
+
expect(prose.textContent.trim().length).toBeGreaterThan(0)
|
|
230
|
+
if (picks.credits) expect(host.textContent).toContain(picks.credits)
|
|
231
|
+
app.unmount()
|
|
232
|
+
}, 20000)
|
|
233
|
+
|
|
234
|
+
it('renders the item sheet on the composed record view', async () => {
|
|
235
|
+
const [items] = await loadEntities(['items'])
|
|
236
|
+
const { app, host } = await mountSite(`#/item/${items[0].id}`)
|
|
237
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-sheet__label')).not.toBeNull(), WAIT)
|
|
238
|
+
expect(host.querySelector('.mwnf-record')).not.toBeNull()
|
|
239
|
+
expect(host.querySelector('.mwnf-dxa-item__languages')).not.toBeNull()
|
|
240
|
+
expect(host.querySelector('.mwnf-sheet-related')).not.toBeNull()
|
|
241
|
+
// Related content is legacy's sidebar, and the record's internal key
|
|
242
|
+
// is never shown.
|
|
243
|
+
expect(host.querySelector('.mwnf-record__aside .mwnf-sheet-related')).not.toBeNull()
|
|
244
|
+
expect(host.textContent).not.toContain('mwnf3:')
|
|
245
|
+
// The "Source database" line names the item's project from the manifest.
|
|
246
|
+
const projectName = manifest.projects[items[0].project_id]?.name?.en ?? ''
|
|
247
|
+
expect(host.querySelector('.mwnf-sheet-source').textContent).toContain(projectName)
|
|
248
|
+
app.unmount()
|
|
249
|
+
}, 60000)
|
|
250
|
+
|
|
251
|
+
outsideReferenceTest(mountSite, manifest, sharedTexts)
|
|
252
|
+
|
|
253
|
+
// The chip's colour and text come from `projectColors` and the manifest's
|
|
254
|
+
// project name, keyed by the item's `project_id`.
|
|
255
|
+
it('colours and names the source-database chip from the manifest projects section', async () => {
|
|
256
|
+
const { app, host } = await mountSite(`#/item/${chip.item}`)
|
|
257
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-sheet-source .mwnf-chip')).not.toBeNull(), WAIT)
|
|
258
|
+
const line = host.querySelector('.mwnf-sheet-source__line')
|
|
259
|
+
expect(line.textContent).toContain(chip.project)
|
|
260
|
+
expect(line.querySelector('.mwnf-chip').classList.contains(chip.className)).toBe(true)
|
|
261
|
+
app.unmount()
|
|
262
|
+
}, 60000)
|
|
263
|
+
|
|
264
|
+
// The Explore-partner notice shows for the projects `noticeProjects`
|
|
265
|
+
// lists, and stays off every other.
|
|
266
|
+
it.skipIf(!noticeItem)('shows the explore-partner notice only for the project dataset.config.js lists', async () => {
|
|
267
|
+
const epm = await mountSite(`#/item/${noticeItem}`)
|
|
268
|
+
await vi.waitFor(() => expect(epm.host.querySelector('.mwnf-sheet-source')).not.toBeNull(), WAIT)
|
|
269
|
+
expect(epm.host.querySelector('.mwnf-sheet-notice')).not.toBeNull()
|
|
270
|
+
epm.app.unmount()
|
|
271
|
+
|
|
272
|
+
const other = await mountSite(`#/item/${chip.item}`)
|
|
273
|
+
await vi.waitFor(() => expect(other.host.querySelector('.mwnf-sheet-source')).not.toBeNull(), WAIT)
|
|
274
|
+
expect(other.host.querySelector('.mwnf-sheet-notice')).toBeNull()
|
|
275
|
+
other.app.unmount()
|
|
276
|
+
}, 60000)
|
|
277
|
+
|
|
278
|
+
// The related-database and artistic-introduction blocks render exactly
|
|
279
|
+
// when the item's project carries the address.
|
|
280
|
+
it('renders the related-database and artistic-introduction links from the manifest', async () => {
|
|
281
|
+
const [items] = await loadEntities(['items'])
|
|
282
|
+
const item = items.find((i) => i.id === chip.item)
|
|
283
|
+
const project = manifest.projects[item.project_id]
|
|
284
|
+
const { app, host } = await mountSite(`#/item/${item.id}`)
|
|
285
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-sheet-related')).not.toBeNull(), WAIT)
|
|
286
|
+
const links = () => Array.from(host.querySelectorAll('.mwnf-sheet-related a'))
|
|
287
|
+
|
|
288
|
+
expect(project.related_database_url).toBeTruthy()
|
|
289
|
+
expect(host.textContent).toContain('Search Related Database')
|
|
290
|
+
expect(links().some((a) => a.getAttribute('href') === project.related_database_url)).toBe(true)
|
|
291
|
+
if (project.artistic_introduction_url) {
|
|
292
|
+
expect(host.textContent).toContain('Artistic Introduction')
|
|
293
|
+
expect(links().some((a) => a.getAttribute('href') === project.artistic_introduction_url)).toBe(true)
|
|
294
|
+
} else {
|
|
295
|
+
expect(host.textContent).not.toContain('Artistic Introduction')
|
|
296
|
+
}
|
|
297
|
+
app.unmount()
|
|
298
|
+
}, 60000)
|
|
299
|
+
|
|
300
|
+
// A borrowed item's tile names its project. A long name is truncated on
|
|
301
|
+
// the tile, so a prefix is matched.
|
|
302
|
+
it('shows the source project on a collection-results tile, from the manifest', async () => {
|
|
303
|
+
const [items] = await loadEntities(['items'])
|
|
304
|
+
const item = items.find((i) => i.id === chip.item)
|
|
305
|
+
const { app, host } = await mountSite(`#/search?q=${encodeURIComponent(item.internal_name.replace(/[*_]/g, ''))}`)
|
|
306
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull(), WAIT)
|
|
307
|
+
expect(host.textContent).toContain(`for project ${chip.project.slice(0, 15)}`)
|
|
308
|
+
app.unmount()
|
|
309
|
+
}, 30000)
|
|
310
|
+
|
|
311
|
+
it('renders the source credit on the item sheet, addressed to this deployed site', async () => {
|
|
312
|
+
const [items] = await loadEntities(['items'])
|
|
313
|
+
const item = items[0]
|
|
314
|
+
const { app, host } = await mountSite(`#/item/${item.id}`)
|
|
315
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-source-credit')).not.toBeNull(), WAIT)
|
|
316
|
+
const link = host.querySelector('.mwnf-source-credit a')
|
|
317
|
+
expect(link.textContent).toBe(link.getAttribute('href'))
|
|
318
|
+
expect(link.getAttribute('href').startsWith(`${config.site.origin}/#/item/${item.id}`)).toBe(true)
|
|
319
|
+
app.unmount()
|
|
320
|
+
}, 60000)
|
|
321
|
+
|
|
322
|
+
it.skipIf(!dynasty)('renders the layout glossary tool and dynasty popouts on the item sheet', async () => {
|
|
323
|
+
const { app, host } = await mountSite(`#/item/${dynasty.item}`)
|
|
324
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-dynasty-list')).not.toBeNull(), WAIT)
|
|
325
|
+
expect(host.querySelector('.mwnf-glossary-tool')).not.toBeNull()
|
|
326
|
+
expect(host.querySelector('.mwnf-dynasty-list__heading').textContent).toContain('Dynasties')
|
|
327
|
+
expect(host.textContent).toContain(dynasty.name)
|
|
328
|
+
app.unmount()
|
|
329
|
+
}, 60000)
|
|
330
|
+
|
|
331
|
+
// The timeline, by the legacy two-letter code and by the inventory id:
|
|
332
|
+
// both addresses produce the same rows.
|
|
333
|
+
for (const [label, country] of [['the legacy code', timeline.code], ['the inventory id', timeline.id]]) {
|
|
334
|
+
it(`renders the timeline results for a country given by ${label}`, async () => {
|
|
335
|
+
const { app, host } = await mountSite(`#/timeline-results?country=${country}`)
|
|
336
|
+
await vi.waitFor(() => expect(host.querySelectorAll('.mwnf-timeline__row').length).toBeGreaterThan(0), WAIT)
|
|
337
|
+
if (timeline.rows) expect(host.querySelectorAll('.mwnf-timeline__row').length).toBe(timeline.rows)
|
|
338
|
+
if (timeline.found ?? timeline.rows) {
|
|
339
|
+
expect(host.querySelector('.mwnf-summary').textContent).toContain(String(timeline.found ?? timeline.rows))
|
|
340
|
+
}
|
|
341
|
+
if (timeline.event) await vi.waitFor(() => expect(host.textContent).toContain(timeline.event), WAIT)
|
|
342
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-timeline__gallery')).not.toBeNull(), WAIT)
|
|
343
|
+
if (timeline.gallery) expect(host.querySelector('.mwnf-timeline__gallery').textContent).toContain(String(timeline.gallery))
|
|
344
|
+
app.unmount()
|
|
345
|
+
}, 60000)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
it('renders the timeline gallery on the composed results view', async () => {
|
|
349
|
+
const { app, host } = await mountSite(`#/timeline/gallery?country=${timeline.code}`)
|
|
350
|
+
await vi.waitFor(() => expect(host.querySelectorAll('.mwnf-grid__tile').length).toBeGreaterThan(0), WAIT)
|
|
351
|
+
if (timeline.galleryTiles) expect(host.querySelectorAll('.mwnf-grid__tile').length).toBe(timeline.galleryTiles)
|
|
352
|
+
expect(host.querySelector('.mwnf-summary').textContent).toContain(timeline.country)
|
|
353
|
+
if (timeline.galleryItem) expect(host.textContent).toContain(timeline.galleryItem)
|
|
354
|
+
app.unmount()
|
|
355
|
+
}, 60000)
|
|
356
|
+
|
|
357
|
+
// The entrance's country control writes the inventory id, and the results
|
|
358
|
+
// it submits to filter on it.
|
|
359
|
+
it("submits the timeline entrance's country control (an id) to a results address that actually filters", async () => {
|
|
360
|
+
const { app, host } = await mountSite('#/timeline')
|
|
361
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-facet__select')).not.toBeNull(), WAIT)
|
|
362
|
+
const countrySelect = host.querySelectorAll('.mwnf-facet__select')[0]
|
|
363
|
+
countrySelect.value = timeline.id
|
|
364
|
+
countrySelect.dispatchEvent(new window.Event('change', { bubbles: true }))
|
|
365
|
+
host.querySelector('form.mwnf-timeline__filters').dispatchEvent(new window.Event('submit', { bubbles: true, cancelable: true }))
|
|
366
|
+
await vi.waitFor(() => expect(host.querySelectorAll('.mwnf-timeline__row').length).toBeGreaterThan(0), WAIT)
|
|
367
|
+
if (timeline.rows) expect(host.querySelectorAll('.mwnf-timeline__row').length).toBe(timeline.rows)
|
|
368
|
+
expect(host.querySelector('.mwnf-timeline__caption').textContent).toContain(timeline.country)
|
|
369
|
+
app.unmount()
|
|
370
|
+
}, 60000)
|
|
371
|
+
|
|
372
|
+
// The object count is the gallery's own entry over the shared one.
|
|
373
|
+
it('renders the partners list on the composed partner-list view', async () => {
|
|
374
|
+
const { app, host } = await mountSite('#/partners')
|
|
375
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-partner-list__row')).not.toBeNull(), WAIT)
|
|
376
|
+
expect(host.textContent).toContain(partner.country)
|
|
377
|
+
expect(host.textContent).toContain(partner.name)
|
|
378
|
+
expect(host.textContent).toContain(`${partner.objects} object(s) in this site`)
|
|
379
|
+
app.unmount()
|
|
380
|
+
}, 60000)
|
|
381
|
+
|
|
382
|
+
it('renders a partner profile on the composed record view', async () => {
|
|
383
|
+
const { app, host } = await mountSite(`#/partner/${partner.id}`)
|
|
384
|
+
// The name and the city arrive with the translations the view loads.
|
|
385
|
+
await vi.waitFor(() => expect(host.textContent).toContain(partner.name), WAIT)
|
|
386
|
+
await vi.waitFor(() => expect(host.textContent).toContain(partner.city), WAIT)
|
|
387
|
+
expect(host.querySelector('.mwnf-partner-map')).not.toBeNull()
|
|
388
|
+
// The credit renders whatever the page's own citation switch says.
|
|
389
|
+
expect(host.querySelector('.mwnf-source-credit')).not.toBeNull()
|
|
390
|
+
app.unmount()
|
|
391
|
+
}, 60000)
|
|
392
|
+
|
|
393
|
+
it("renders a partner's objects on the composed grid results view", async () => {
|
|
394
|
+
const { app, host } = await mountSite(`#/partner/${partner.id}/objects`)
|
|
395
|
+
await vi.waitFor(() => expect(host.querySelectorAll('.mwnf-grid__tile').length).toBe(partner.tiles ?? partner.objects), WAIT)
|
|
396
|
+
expect(host.textContent).toContain(partner.name)
|
|
397
|
+
app.unmount()
|
|
398
|
+
}, 60000)
|
|
399
|
+
|
|
400
|
+
configTests(config, {
|
|
401
|
+
names: [
|
|
402
|
+
'home', 'collection', 'collection-results', 'item', 'search-results', 'search-how-to',
|
|
403
|
+
'partners', 'partner', 'partner-objects', 'timeline', 'timeline-results', 'timeline-gallery',
|
|
404
|
+
'about', 'credits',
|
|
405
|
+
],
|
|
406
|
+
legacyPaths: [
|
|
407
|
+
'/database-item/:uid(.*)/:language',
|
|
408
|
+
'/partner/:country/:id/:language',
|
|
409
|
+
'/partner-objects/:country/:id/:page',
|
|
410
|
+
'/timeline-gallery/:country/:start/:end/:page',
|
|
411
|
+
],
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// Every deep link and legacy redirect targets these name/path pairs, so
|
|
415
|
+
// they must not move.
|
|
416
|
+
it('pins every route name to its path', () => {
|
|
417
|
+
const routes = config.extraViews
|
|
418
|
+
.map((r) => ({ name: r.name, path: r.path }))
|
|
419
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
420
|
+
expect(routes).toEqual([
|
|
421
|
+
{ name: 'about', path: '/about' },
|
|
422
|
+
{ name: 'collection', path: '/collection' },
|
|
423
|
+
{ name: 'collection-results', path: '/collection-results' },
|
|
424
|
+
{ name: 'credits', path: '/credits' },
|
|
425
|
+
{ name: 'home', path: '/' },
|
|
426
|
+
{ name: 'item', path: '/item/:id' },
|
|
427
|
+
{ name: 'partner', path: '/partner/:id' },
|
|
428
|
+
{ name: 'partner-objects', path: '/partner/:id/objects' },
|
|
429
|
+
{ name: 'partners', path: '/partners' },
|
|
430
|
+
{ name: 'search-how-to', path: '/how-to-search' },
|
|
431
|
+
{ name: 'search-results', path: '/search' },
|
|
432
|
+
{ name: 'timeline', path: '/timeline' },
|
|
433
|
+
{ name: 'timeline-gallery', path: '/timeline/gallery' },
|
|
434
|
+
{ name: 'timeline-results', path: '/timeline-results' },
|
|
435
|
+
])
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// `SiteShell` marks a menu entry active by comparing its section to the
|
|
439
|
+
// current route's.
|
|
440
|
+
it('marks the current section active in the menu, and renders the header, footer and search', async () => {
|
|
441
|
+
const { app, host } = await mountSite('#/partners')
|
|
442
|
+
await vi.waitFor(() => expect(host.querySelector('a[href="#/partners"]')).not.toBeNull(), WAIT)
|
|
443
|
+
const active = host.querySelector('.mwnf-nav__link--active')
|
|
444
|
+
expect(active?.getAttribute('href')).toBe('#/partners')
|
|
445
|
+
expect(active?.getAttribute('aria-current')).toBe('page')
|
|
446
|
+
expect(host.querySelector('a[href="#/timeline"]')?.classList.contains('mwnf-nav__link--active')).toBe(false)
|
|
447
|
+
expect(host.querySelector('a[href="#/"]')).not.toBeNull()
|
|
448
|
+
expect(host.querySelector('a[href="https://www.museumwnf.org/about"]')).not.toBeNull()
|
|
449
|
+
expect(host.querySelector('.mwnf-header__search-input')).not.toBeNull()
|
|
450
|
+
app.unmount()
|
|
451
|
+
}, 20000)
|
|
452
|
+
|
|
453
|
+
// What a gallery contributes to a legacy address is the mapping; that the
|
|
454
|
+
// router turns it into a redirect is viewer-core's own test.
|
|
455
|
+
it('maps a legacy address onto the canonical route', async () => {
|
|
456
|
+
const [items, partners] = await loadEntities(['items', 'partners'])
|
|
457
|
+
const [itemFor, partnerFor, objectsFor, galleryFor] = config.legacyRoutes
|
|
458
|
+
|
|
459
|
+
const item = items.find((i) => i.backward_compatibility)
|
|
460
|
+
expect(await itemFor.resolve({ uid: item.backward_compatibility.split(':').join('/') })).toEqual({
|
|
461
|
+
name: 'item',
|
|
462
|
+
params: { id: item.id },
|
|
463
|
+
})
|
|
464
|
+
expect(await itemFor.resolve({ uid: 'mwnf3/objects/NOPE/xx/Mus00/0' })).toBeNull()
|
|
465
|
+
|
|
466
|
+
const legacyPartner = partners.find((p) => (p.backward_compatibility ?? '').split(':').length >= 4)
|
|
467
|
+
const [, , legacyId, country] = legacyPartner.backward_compatibility.split(':')
|
|
468
|
+
expect(await partnerFor.resolve({ country, id: legacyId })).toEqual({
|
|
469
|
+
name: 'partner',
|
|
470
|
+
params: { id: legacyPartner.id },
|
|
471
|
+
})
|
|
472
|
+
expect(await objectsFor.resolve({ country, id: legacyId, page: '3' })).toEqual({
|
|
473
|
+
name: 'partner-objects',
|
|
474
|
+
params: { id: legacyPartner.id },
|
|
475
|
+
query: { page: '3' },
|
|
476
|
+
})
|
|
477
|
+
// The page number and the period leave the path for the query, and an
|
|
478
|
+
// open bound stops being the literal 'any'.
|
|
479
|
+
expect(galleryFor.resolve({ country: 'uk', start: 'any', end: '1500', page: '2' })).toEqual({
|
|
480
|
+
name: 'timeline-gallery',
|
|
481
|
+
query: { country: 'uk', end: '1500', page: '2' },
|
|
482
|
+
})
|
|
483
|
+
}, 20000)
|
|
484
|
+
|
|
485
|
+
// A missing text renders as its own name rather than as an error, so the
|
|
486
|
+
// rendered page is searched for one, in every namespace a page reads.
|
|
487
|
+
it('renders the shared texts and its own over them', async () => {
|
|
488
|
+
const { app, host } = await mountSite()
|
|
489
|
+
const text = host.textContent
|
|
490
|
+
expect(text).toContain('Skip to content')
|
|
491
|
+
expect(text).toContain('All MWNF Galleries')
|
|
492
|
+
expect(text).toContain('Tip:')
|
|
493
|
+
expect(checkTextsRendered(spacedText(host), {
|
|
494
|
+
namespaces: [namespace, 'core', 'layout', 'catalogue', 'record', 'sheet', 'timeline', 'partner', 'gallery'],
|
|
495
|
+
})).toEqual([])
|
|
496
|
+
app.unmount()
|
|
497
|
+
}, 20000)
|
|
498
|
+
|
|
499
|
+
rightsTest(mountSite, manifest, messages)
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The smoke test every DXA exhibition runs. It finds the records it needs in
|
|
505
|
+
* the exhibition's own package.
|
|
506
|
+
*
|
|
507
|
+
* - `config` with its `noticeProjects` and `projectColors`, `sharedTexts`
|
|
508
|
+
* (viewer-i18n's exhibition catalogues), `ownTexts` (locales/en.json),
|
|
509
|
+
* `manifest`, `partnerNames` and `dynastyNames` (the package's English
|
|
510
|
+
* partner and dynasty translations) and `namespace` (this exhibition's own
|
|
511
|
+
* text namespace).
|
|
512
|
+
* - Optionally, `collection: { tiles, paginations }`; `theme: { id,
|
|
513
|
+
* pictureTitle, subthemes }`, a theme whose first picture and sub-theme
|
|
514
|
+
* titles the theme page shows; `relatedContent: true` when the
|
|
515
|
+
* related-content page must list entries; and `dynasties: false` for an
|
|
516
|
+
* exhibition no dynasty of which has a translated history.
|
|
517
|
+
*/
|
|
518
|
+
export function describeExhibitionSmoke({
|
|
519
|
+
config, noticeProjects, projectColors, sharedTexts, ownTexts, manifest, partnerNames, dynastyNames, namespace,
|
|
520
|
+
collection, theme: themePick, relatedContent = false, dynasties = true,
|
|
521
|
+
}) {
|
|
522
|
+
const { messages, mountSite } = mounter(config, sharedTexts, ownTexts)
|
|
523
|
+
const loadItems = async () => (await loadEntities(['exhibition', 'items']))[1]
|
|
524
|
+
const visiblePartners = async () => {
|
|
525
|
+
const [exhibition, partners] = await loadEntities(['exhibition', 'partners'])
|
|
526
|
+
const hidden = new Set(exhibition.hidden_partner_ids ?? [])
|
|
527
|
+
return partners.filter((p) => !hidden.has(p.id))
|
|
528
|
+
}
|
|
529
|
+
const datedItem = async () => (await loadItems()).find((i) => Number.isFinite(i.start_date) && i.country_id)
|
|
530
|
+
|
|
531
|
+
describe('exhibition smoke test', () => {
|
|
532
|
+
it('mounts against the configured data package', async () => {
|
|
533
|
+
const { app, host } = await mountSite()
|
|
534
|
+
expect(host.textContent).toContain(config.siteName)
|
|
535
|
+
expect(host.querySelector('.mwnf-page')).not.toBeNull()
|
|
536
|
+
expect(host.querySelector('.vc-home')).toBeNull()
|
|
537
|
+
app.unmount()
|
|
538
|
+
}, 20000)
|
|
539
|
+
|
|
540
|
+
collectionTest(mountSite, collection)
|
|
541
|
+
|
|
542
|
+
it('renders the item sheet on the composed record view', async () => {
|
|
543
|
+
const items = await loadItems()
|
|
544
|
+
const item = items.find((i) => i.project_id) ?? items[0]
|
|
545
|
+
const { app, host } = await mountSite(`#/item/${item.id}`)
|
|
546
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-sheet__label')).not.toBeNull(), WAIT)
|
|
547
|
+
expect(host.querySelector('.mwnf-record')).not.toBeNull()
|
|
548
|
+
expect(host.querySelector('.mwnf-dxa-item__languages')).not.toBeNull()
|
|
549
|
+
expect(host.querySelector('.mwnf-sheet-related')).not.toBeNull()
|
|
550
|
+
// Related content is legacy's sidebar, and the record's internal key
|
|
551
|
+
// is never shown.
|
|
552
|
+
expect(host.querySelector('.mwnf-record__aside .mwnf-sheet-related')).not.toBeNull()
|
|
553
|
+
expect(host.textContent).not.toContain('mwnf3:')
|
|
554
|
+
const projectName = manifest.projects?.[item.project_id]?.name?.en
|
|
555
|
+
if (projectName) expect(host.querySelector('.mwnf-sheet-source__line').textContent).toContain(projectName)
|
|
556
|
+
// The glossary tool is the layout's own, on every sheet.
|
|
557
|
+
expect(host.querySelector('.mwnf-glossary-tool')).not.toBeNull()
|
|
558
|
+
// The source credit: origin plus this item's route.
|
|
559
|
+
const creditLink = host.querySelector('.mwnf-source-credit a')
|
|
560
|
+
expect(creditLink).not.toBeNull()
|
|
561
|
+
expect(creditLink.textContent.startsWith(config.site.origin)).toBe(true)
|
|
562
|
+
expect(creditLink.textContent.endsWith(`#/item/${item.id}`)).toBe(true)
|
|
563
|
+
app.unmount()
|
|
564
|
+
}, 60000)
|
|
565
|
+
|
|
566
|
+
// One sheet per project the package references, so every gate value is
|
|
567
|
+
// exercised: an absent address is as provable as a present one.
|
|
568
|
+
it('gates the related-database link, the Artistic Introduction link and the Explore-partner notice on the manifest project', async () => {
|
|
569
|
+
const items = await loadItems()
|
|
570
|
+
const seen = new Set()
|
|
571
|
+
const sample = items.filter((i) => {
|
|
572
|
+
if (!i.project_id || seen.has(i.project_id) || !manifest.projects?.[i.project_id]) return false
|
|
573
|
+
seen.add(i.project_id)
|
|
574
|
+
return true
|
|
575
|
+
})
|
|
576
|
+
expect(sample.length).toBeGreaterThan(0)
|
|
577
|
+
for (const item of sample) {
|
|
578
|
+
const project = manifest.projects[item.project_id]
|
|
579
|
+
const { app, host } = await mountSite(`#/item/${item.id}`)
|
|
580
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-sheet-related')).not.toBeNull(), WAIT)
|
|
581
|
+
expect(!!host.querySelector(`a[href="${project.related_database_url}"]`)).toBe(!!project.related_database_url)
|
|
582
|
+
expect(!!host.querySelector(`a[href="${project.artistic_introduction_url}"]`)).toBe(!!project.artistic_introduction_url)
|
|
583
|
+
expect(!!host.querySelector('.mwnf-sheet-notice')).toBe(noticeProjects.includes(item.project_id))
|
|
584
|
+
// Every project needs its chip colour, or the chip falls back to the
|
|
585
|
+
// layout's default swatch.
|
|
586
|
+
expect(projectColors[item.project_id]).toBeTruthy()
|
|
587
|
+
app.unmount()
|
|
588
|
+
}
|
|
589
|
+
}, 120000)
|
|
590
|
+
|
|
591
|
+
// A dynasty with a history gets a popout; one without gets none.
|
|
592
|
+
it.skipIf(!dynasties)('renders a dynasty popout on an item sheet that has one', async () => {
|
|
593
|
+
const items = await loadItems()
|
|
594
|
+
const item = items.find((i) =>
|
|
595
|
+
(!i.languages?.length || i.languages.includes('en'))
|
|
596
|
+
&& (i.dynasty_ids ?? []).some((id) => dynastyNames[id]?.history))
|
|
597
|
+
expect(item).toBeTruthy()
|
|
598
|
+
const dynastyId = item.dynasty_ids.find((id) => dynastyNames[id]?.history)
|
|
599
|
+
const { app, host } = await mountSite(`#/item/${item.id}`)
|
|
600
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-dynasty-list')).not.toBeNull(), WAIT)
|
|
601
|
+
expect(host.querySelectorAll('.mwnf-dynasty').length).toBeGreaterThan(0)
|
|
602
|
+
expect(host.textContent).toContain(dynastyNames[dynastyId].name)
|
|
603
|
+
app.unmount()
|
|
604
|
+
}, 60000)
|
|
605
|
+
|
|
606
|
+
it('renders the partners list on the composed list view', async () => {
|
|
607
|
+
const partner = (await visiblePartners()).find((p) => partnerNames[p.id]?.name)
|
|
608
|
+
const { app, host } = await mountSite('#/partners')
|
|
609
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-partner-list')).not.toBeNull(), WAIT)
|
|
610
|
+
// Country groups, all open, and the A-Z / Z-A toggle.
|
|
611
|
+
expect(host.querySelectorAll('.mwnf-partner-list__group-heading').length).toBeGreaterThan(0)
|
|
612
|
+
expect(host.querySelector('.mwnf-partner-list__toggle-button')).not.toBeNull()
|
|
613
|
+
expect(host.textContent).toContain(partnerNames[partner.id].name)
|
|
614
|
+
app.unmount()
|
|
615
|
+
}, 30000)
|
|
616
|
+
|
|
617
|
+
it('renders a partner profile on the composed record view', async () => {
|
|
618
|
+
const partner = (await visiblePartners()).find((p) => p.type !== 'institution' && partnerNames[p.id]?.name)
|
|
619
|
+
const { app, host } = await mountSite(`#/partner/${partner.id}`)
|
|
620
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-record')).not.toBeNull(), WAIT)
|
|
621
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-record').textContent.trim().length).toBeGreaterThan(0), WAIT)
|
|
622
|
+
// The tab strip and the map are the page's own slots.
|
|
623
|
+
expect(host.querySelector('[role="tablist"], .mwnf-dxa-profile-links')).not.toBeNull()
|
|
624
|
+
expect(host.querySelector('.mwnf-partner-map')).not.toBeNull()
|
|
625
|
+
app.unmount()
|
|
626
|
+
}, 30000)
|
|
627
|
+
|
|
628
|
+
it('renders a partner objects page on the composed results view', async () => {
|
|
629
|
+
const partner = (await visiblePartners()).find((p) => p.item_count > 0)
|
|
630
|
+
const { app, host } = await mountSite(`#/partner/${partner.id}/objects`)
|
|
631
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull(), WAIT)
|
|
632
|
+
expect(host.querySelector('.mwnf-catalogue')).not.toBeNull()
|
|
633
|
+
expect(host.textContent).toContain(partnerNames[partner.id]?.name ?? partner.id)
|
|
634
|
+
app.unmount()
|
|
635
|
+
}, 30000)
|
|
636
|
+
|
|
637
|
+
it('renders the collection entrance on the composed search form view', async () => {
|
|
638
|
+
const { app, host } = await mountSite('#/collection')
|
|
639
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-search-form')).not.toBeNull(), WAIT)
|
|
640
|
+
expect(host.querySelectorAll('.mwnf-search-form .mwnf-facet').length).toBeGreaterThan(1)
|
|
641
|
+
expect(host.querySelector('.mwnf-search-form__dates')).not.toBeNull()
|
|
642
|
+
expect(host.querySelector('.mwnf-search-form__how-to')).not.toBeNull()
|
|
643
|
+
expect(host.textContent).toContain('Have you already been at')
|
|
644
|
+
app.unmount()
|
|
645
|
+
}, 30000)
|
|
646
|
+
|
|
647
|
+
searchTests(mountSite, async () => (await loadItems()).find((i) => !i.languages?.length || i.languages.includes('en')))
|
|
648
|
+
|
|
649
|
+
it('renders the themes list on the composed accordion', async () => {
|
|
650
|
+
const { app, host } = await mountSite('#/themes')
|
|
651
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-cards--accordion')).not.toBeNull(), WAIT)
|
|
652
|
+
expect(host.querySelectorAll('.mwnf-cards__details').length).toBeGreaterThan(0)
|
|
653
|
+
app.unmount()
|
|
654
|
+
}, 30000)
|
|
655
|
+
|
|
656
|
+
it('renders a theme on the composed essay view', async () => {
|
|
657
|
+
const [, , , themes] = await loadEntities(['exhibition', 'items', 'partners', 'themes'])
|
|
658
|
+
const theme = themePick
|
|
659
|
+
? themes.find((t) => t.id === themePick.id)
|
|
660
|
+
: themes.find((t) => t.display_order > 1) ?? themes[0]
|
|
661
|
+
const { app, host } = await mountSite(`#/theme/${themePick ? themePick.id : theme.display_order - 1}`)
|
|
662
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-essay')).not.toBeNull(), WAIT)
|
|
663
|
+
expect(host.querySelector('.mwnf-essay').className).not.toContain('mwnf-essay--about')
|
|
664
|
+
// The Roman label beside the theme's own title.
|
|
665
|
+
expect(host.querySelector('.mwnf-dxa-theme__heading').textContent).toMatch(/[IVX]/)
|
|
666
|
+
expect(host.querySelector('.mwnf-essay__side')).not.toBeNull()
|
|
667
|
+
expect(host.querySelector('.mwnf-picture-gallery__selected, .mwnf-picture-gallery__empty')).not.toBeNull()
|
|
668
|
+
expect(host.querySelector('.mwnf-essay__nav')).not.toBeNull()
|
|
669
|
+
// The selected picture's caption is its parent record's label.
|
|
670
|
+
if (themePick?.pictureTitle) {
|
|
671
|
+
expect(host.querySelector('.mwnf-picture-gallery__detail--title').textContent).toContain(themePick.pictureTitle)
|
|
672
|
+
} else {
|
|
673
|
+
expect(host.querySelector('.mwnf-picture-gallery__detail--title, .mwnf-picture-gallery__empty')).not.toBeNull()
|
|
674
|
+
}
|
|
675
|
+
const subNav = host.querySelector('.mwnf-dxa-theme__subthemes')
|
|
676
|
+
for (const title of themePick?.subthemes ?? []) expect(subNav.textContent).toContain(title)
|
|
677
|
+
if (subNav) expect(subNav.textContent.trim().length).toBeGreaterThan(0)
|
|
678
|
+
// The texts are read through the tree's own entity: a wrong one renders
|
|
679
|
+
// internal names or nothing.
|
|
680
|
+
const prose = host.querySelector('.mwnf-essay__prose, .mwnf-essay__body')
|
|
681
|
+
expect(prose?.textContent?.trim()).toBeTruthy()
|
|
682
|
+
const title = host.querySelector('.mwnf-essay__title')
|
|
683
|
+
if (title && theme) expect(title.textContent).not.toBe(theme.internal_name)
|
|
684
|
+
const creditLink = host.querySelector('.mwnf-source-credit a')
|
|
685
|
+
expect(creditLink).not.toBeNull()
|
|
686
|
+
expect(creditLink.textContent.startsWith(config.site.origin)).toBe(true)
|
|
687
|
+
app.unmount()
|
|
688
|
+
}, 30000)
|
|
689
|
+
|
|
690
|
+
it('renders the theme gallery on the composed results view', async () => {
|
|
691
|
+
const { app, host } = await mountSite('#/theme-gallery/1')
|
|
692
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull(), WAIT)
|
|
693
|
+
expect(host.querySelector('.mwnf-catalogue')).not.toBeNull()
|
|
694
|
+
app.unmount()
|
|
695
|
+
}, 30000)
|
|
696
|
+
|
|
697
|
+
it('renders the timeline entrance on the composed timeline view, as a form', async () => {
|
|
698
|
+
const { app, host } = await mountSite('#/timeline')
|
|
699
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-timeline')).not.toBeNull(), WAIT)
|
|
700
|
+
expect(host.querySelector('.mwnf-timeline__filters')).not.toBeNull()
|
|
701
|
+
// The entrance renders the form alone: no results row, no summary.
|
|
702
|
+
expect(host.querySelector('.mwnf-timeline__row')).toBeNull()
|
|
703
|
+
expect(host.querySelector('.mwnf-summary')).toBeNull()
|
|
704
|
+
expect(host.textContent).toContain('Have you already been at')
|
|
705
|
+
app.unmount()
|
|
706
|
+
}, 30000)
|
|
707
|
+
|
|
708
|
+
it('renders the timeline results on the composed timeline view', async () => {
|
|
709
|
+
const { app, host } = await mountSite('#/timeline-results')
|
|
710
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-summary')).not.toBeNull(), WAIT)
|
|
711
|
+
expect(host.querySelector('.mwnf-timeline')).not.toBeNull()
|
|
712
|
+
expect(host.textContent).toContain('Events found')
|
|
713
|
+
app.unmount()
|
|
714
|
+
}, 30000)
|
|
715
|
+
|
|
716
|
+
it('offers "See Gallery" from the timeline results when the period has objects', async () => {
|
|
717
|
+
const dated = await datedItem()
|
|
718
|
+
const { app, host } = await mountSite(
|
|
719
|
+
`#/timeline-results?country=${dated.country_id}&begin=${dated.start_date}&end=${dated.end_date ?? dated.start_date}`,
|
|
720
|
+
)
|
|
721
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-timeline__gallery')).not.toBeNull(), WAIT)
|
|
722
|
+
expect(host.querySelector('.mwnf-timeline__gallery').textContent).toContain('See Gallery')
|
|
723
|
+
app.unmount()
|
|
724
|
+
}, 30000)
|
|
725
|
+
|
|
726
|
+
it('renders the timeline gallery on the composed results view', async () => {
|
|
727
|
+
const dated = await datedItem()
|
|
728
|
+
const { app, host } = await mountSite(
|
|
729
|
+
`#/timeline/gallery?country=${dated.country_id}&begin=${dated.start_date}&end=${dated.end_date ?? dated.start_date}`,
|
|
730
|
+
)
|
|
731
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-catalogue')).not.toBeNull(), WAIT)
|
|
732
|
+
// The item that seeded the query overlaps its own period.
|
|
733
|
+
expect(host.querySelector('.mwnf-grid__tile')).not.toBeNull()
|
|
734
|
+
app.unmount()
|
|
735
|
+
}, 30000)
|
|
736
|
+
|
|
737
|
+
// The inventory id and the legacy two-letter code filter the same rows.
|
|
738
|
+
it('timeline country id filter produces the same results as the legacy two-letter code', async () => {
|
|
739
|
+
const items = await loadItems()
|
|
740
|
+
const greek = items.find((i) => Number.isFinite(i.start_date) && i.country_id === 'grc')
|
|
741
|
+
if (!greek) return
|
|
742
|
+
const period = `begin=${greek.start_date}&end=${greek.end_date ?? greek.start_date}`
|
|
743
|
+
const byId = await mountSite(`#/timeline-results?country=grc&${period}`)
|
|
744
|
+
await vi.waitFor(() => expect(byId.host.querySelector('.mwnf-summary')).not.toBeNull(), WAIT)
|
|
745
|
+
const byCode = await mountSite(`#/timeline-results?country=gr&${period}`)
|
|
746
|
+
await vi.waitFor(() => expect(byCode.host.querySelector('.mwnf-summary')).not.toBeNull(), WAIT)
|
|
747
|
+
expect(byId.host.querySelectorAll('.mwnf-timeline__row').length).toBe(byCode.host.querySelectorAll('.mwnf-timeline__row').length)
|
|
748
|
+
const galleryId = byId.host.querySelector('.mwnf-timeline__gallery')
|
|
749
|
+
const galleryCode = byCode.host.querySelector('.mwnf-timeline__gallery')
|
|
750
|
+
if (galleryId && galleryCode) expect(galleryId.textContent).toBe(galleryCode.textContent)
|
|
751
|
+
const firstRow = byId.host.querySelector('.mwnf-timeline__row')
|
|
752
|
+
if (firstRow) expect(firstRow.textContent).toContain('Greece')
|
|
753
|
+
byId.app.unmount()
|
|
754
|
+
byCode.app.unmount()
|
|
755
|
+
}, 60000)
|
|
756
|
+
|
|
757
|
+
it('renders the related content on the composed link list', async () => {
|
|
758
|
+
const { app, host } = await mountSite('#/related')
|
|
759
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-link-list')).not.toBeNull(), WAIT)
|
|
760
|
+
if (relatedContent) {
|
|
761
|
+
expect(host.querySelector('.mwnf-link-list__groups')).not.toBeNull()
|
|
762
|
+
expect(host.querySelectorAll('.mwnf-link-list__item').length).toBeGreaterThan(0)
|
|
763
|
+
} else {
|
|
764
|
+
expect(host.querySelector('.mwnf-link-list__groups, .mwnf-link-list__empty')).not.toBeNull()
|
|
765
|
+
}
|
|
766
|
+
app.unmount()
|
|
767
|
+
}, 30000)
|
|
768
|
+
|
|
769
|
+
it('renders about on the composed essay view, in about mode', async () => {
|
|
770
|
+
const { app, host } = await mountSite('#/about')
|
|
771
|
+
await vi.waitFor(() => expect(host.querySelector('.mwnf-essay')).not.toBeNull(), WAIT)
|
|
772
|
+
expect(host.querySelector('.mwnf-essay').className).toContain('mwnf-essay--about')
|
|
773
|
+
// About mode keeps legacy's picture column (the About theme's pictures
|
|
774
|
+
// and their captions) and drops the tour and the picture narrative.
|
|
775
|
+
expect(host.querySelector('.mwnf-essay__side .mwnf-picture-gallery')).not.toBeNull()
|
|
776
|
+
expect(host.querySelector('.mwnf-picture-narrative')).toBeNull()
|
|
777
|
+
expect(host.textContent).toContain(config.siteName)
|
|
778
|
+
app.unmount()
|
|
779
|
+
}, 30000)
|
|
780
|
+
|
|
781
|
+
configTests(config, {
|
|
782
|
+
names: [
|
|
783
|
+
'home', 'about', 'themes', 'theme', 'theme-gallery', 'collection', 'collection-results',
|
|
784
|
+
'item', 'search-results', 'search-how-to', 'partners', 'partner', 'partner-objects',
|
|
785
|
+
'institution', 'institution-monuments', 'related', 'timeline', 'timeline-results',
|
|
786
|
+
'timeline-gallery', 'credits',
|
|
787
|
+
],
|
|
788
|
+
legacyPaths: [
|
|
789
|
+
'/database-item/:uid(.*)/:language',
|
|
790
|
+
'/partner/:country/:id/:language',
|
|
791
|
+
'/partner-objects/:country/:id/:page',
|
|
792
|
+
'/institution/:country/:id/:language',
|
|
793
|
+
'/institution-monuments/:country/:id/:page',
|
|
794
|
+
'/timeline-gallery/:country/:start/:end/:page',
|
|
795
|
+
],
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
it('maps a legacy address onto the canonical route', async () => {
|
|
799
|
+
const [, items, partners] = await loadEntities(['exhibition', 'items', 'partners'])
|
|
800
|
+
const [itemFor, partnerFor, objectsFor, institutionFor, monumentsFor, galleryFor] = config.legacyRoutes
|
|
801
|
+
|
|
802
|
+
const item = items.find((i) => i.backward_compatibility)
|
|
803
|
+
expect(await itemFor.resolve({ uid: item.backward_compatibility.split(':').join('/') })).toEqual({
|
|
804
|
+
name: 'item',
|
|
805
|
+
params: { id: item.id },
|
|
806
|
+
})
|
|
807
|
+
expect(await itemFor.resolve({ uid: 'mwnf3/objects/NOPE/xx/Mus00/0' })).toBeNull()
|
|
808
|
+
|
|
809
|
+
const museum = partners.find((p) => p.type !== 'institution' && (p.backward_compatibility ?? '').split(':').length >= 4)
|
|
810
|
+
const [, , museumId, museumCountry] = museum.backward_compatibility.split(':')
|
|
811
|
+
expect(await partnerFor.resolve({ country: museumCountry, id: museumId })).toEqual({
|
|
812
|
+
name: 'partner',
|
|
813
|
+
params: { id: museum.id },
|
|
814
|
+
})
|
|
815
|
+
expect(await objectsFor.resolve({ country: museumCountry, id: museumId, page: '3' })).toEqual({
|
|
816
|
+
name: 'partner-objects',
|
|
817
|
+
params: { id: museum.id },
|
|
818
|
+
query: { page: '3' },
|
|
819
|
+
})
|
|
820
|
+
// An institution reaches the institution pages, the same component
|
|
821
|
+
// under another name.
|
|
822
|
+
expect(await institutionFor.resolve({ country: museumCountry, id: museumId })).toEqual({
|
|
823
|
+
name: 'institution',
|
|
824
|
+
params: { id: museum.id },
|
|
825
|
+
})
|
|
826
|
+
expect(await monumentsFor.resolve({ country: museumCountry, id: museumId, page: '1' })).toEqual({
|
|
827
|
+
name: 'institution-monuments',
|
|
828
|
+
params: { id: museum.id },
|
|
829
|
+
query: {},
|
|
830
|
+
})
|
|
831
|
+
// The page number and the period leave the path for the query; an open
|
|
832
|
+
// bound stops being the literal 'any', and `start` becomes `begin`.
|
|
833
|
+
expect(galleryFor.resolve({ country: 'uk', start: 'any', end: '1500', page: '2' })).toEqual({
|
|
834
|
+
name: 'timeline-gallery',
|
|
835
|
+
query: { country: 'uk', end: '1500', page: '2' },
|
|
836
|
+
})
|
|
837
|
+
expect(galleryFor.resolve({ country: 'uk', start: '1200', end: 'any', page: '1' })).toEqual({
|
|
838
|
+
name: 'timeline-gallery',
|
|
839
|
+
query: { country: 'uk', begin: '1200' },
|
|
840
|
+
})
|
|
841
|
+
}, 20000)
|
|
842
|
+
|
|
843
|
+
it('renders the shared texts and its own over them', async () => {
|
|
844
|
+
const { app, host } = await mountSite()
|
|
845
|
+
const text = host.textContent
|
|
846
|
+
expect(text).toContain('Skip to content')
|
|
847
|
+
expect(text).toContain('Themes')
|
|
848
|
+
expect(text).toContain('A MWNF online exhibition.')
|
|
849
|
+
expect(checkTextsRendered(spacedText(host), {
|
|
850
|
+
namespaces: [namespace, 'core', 'layout', 'catalogue', 'record', 'sheet', 'timeline', 'partner', 'exhibition'],
|
|
851
|
+
})).toEqual([])
|
|
852
|
+
app.unmount()
|
|
853
|
+
}, 20000)
|
|
854
|
+
|
|
855
|
+
rightsTest(mountSite, manifest, messages)
|
|
856
|
+
})
|
|
857
|
+
}
|