From 8969b10130e224c450fee40d5182c0bd2ae14e2b Mon Sep 17 00:00:00 2001 From: Stefan Probst Date: Sat, 22 Jun 2024 13:39:21 +0200 Subject: [PATCH 1/3] chore: update deps and tests --- app.vue | 1 + components/app-footer.vue | 6 +- components/data-map-view.vue | 12 +- components/data-view.vue | 33 +- components/geo-map.client.vue | 4 +- components/imprint-acdh-ch.vue | 2 +- components/route-announcer.vue | 47 - composables/use-api-client.ts | 2 +- composables/use-get-search-results.ts | 56 +- composables/use-id-prefix.ts | 4 +- config/project.config.ts | 82 +- e2e/lib/fixtures/a11y.ts | 24 + e2e/lib/fixtures/i18n.ts | 27 + e2e/lib/fixtures/imprint-page.ts | 13 +- e2e/lib/fixtures/index-page.ts | 13 +- e2e/lib/test.ts | 81 +- e2e/tests/app/analytics.test.ts | 8 +- e2e/tests/app/app.test.ts | 72 +- e2e/tests/app/i18n.test.ts | 71 +- e2e/tests/app/metadata.test.ts | 148 +- e2e/tests/pages/imprint.test.ts | 15 +- e2e/tests/pages/index.test.ts | 13 +- layouts/default.vue | 18 +- nuxt.config.ts | 36 +- package.json | 127 +- pages/data/index.vue | 4 +- pages/entities/[id].vue | 10 +- pages/index.vue | 7 +- pages/map/index.vue | 4 +- pages/network/index.vue | 4 +- pnpm-lock.yaml | 7108 +++++++++++++------------ project.config.json | 32 +- scripts/generate-api-client.ts | 12 +- server/plugins/content.ts | 2 +- server/routes/robots.txt.get.ts | 6 +- server/routes/sitemap.xml.get.ts | 4 +- utils/safe-json-ld-replacer.ts | 6 +- 37 files changed, 4323 insertions(+), 3791 deletions(-) delete mode 100644 components/route-announcer.vue create mode 100644 e2e/lib/fixtures/a11y.ts create mode 100644 e2e/lib/fixtures/i18n.ts diff --git a/app.vue b/app.vue index 1a892617..186967f7 100644 --- a/app.vue +++ b/app.vue @@ -2,5 +2,6 @@ + diff --git a/components/app-footer.vue b/components/app-footer.vue index 570eb83e..f1b15c92 100644 --- a/components/app-footer.vue +++ b/components/app-footer.vue @@ -47,11 +47,7 @@ const links = computed(() => {
Version: - {{ - [env.public.NUXT_PUBLIC_GIT_TAG, env.public.NUXT_PUBLIC_GIT_BRANCH_NAME] - .filter(isNonEmptyString) - .join(" - ") - }} + {{ [env.public.gitTag, env.public.gitBranchName].filter(isNonEmptyString).join(" - ") }}
diff --git a/components/data-map-view.vue b/components/data-map-view.vue index dba30ce5..02f6e1eb 100644 --- a/components/data-map-view.vue +++ b/components/data-map-view.vue @@ -2,7 +2,7 @@ import { keyByToMap } from "@acdh-oeaw/lib"; import * as turf from "@turf/turf"; import type { MapGeoJSONFeature } from "maplibre-gl"; -import { z } from "zod"; +import * as v from "valibot"; import type { SearchFormData } from "@/components/search-form.vue"; import type { EntityFeature } from "@/composables/use-create-entity"; @@ -15,16 +15,16 @@ const router = useRouter(); const route = useRoute(); const t = useTranslations(); -const searchFiltersSchema = z.object({ - category: z.enum(categories).catch("entityName"), - search: z.string().catch(""), +const searchFiltersSchema = v.object({ + category: v.fallback(v.picklist(categories), "entityName"), + search: v.fallback(v.string(), ""), }); const searchFilters = computed(() => { - return searchFiltersSchema.parse(route.query); + return v.parse(searchFiltersSchema, route.query); }); -type SearchFilters = z.infer; +type SearchFilters = v.InferOutput; function setSearchFilters(query: Partial) { void router.push({ query }); diff --git a/components/data-view.vue b/components/data-view.vue index 41c21264..b19d68cf 100644 --- a/components/data-view.vue +++ b/components/data-view.vue @@ -1,6 +1,6 @@ - - diff --git a/composables/use-api-client.ts b/composables/use-api-client.ts index 1cb25449..c198c409 100644 --- a/composables/use-api-client.ts +++ b/composables/use-api-client.ts @@ -4,7 +4,7 @@ import type { paths } from "@/lib/api-client/api"; export function useApiClient() { const env = useRuntimeConfig(); - const baseUrl = env.public.NUXT_PUBLIC_API_BASE_URL; + const baseUrl = env.public.apiBaseUrl; const client = createApiClient({ baseUrl }); return client; diff --git a/composables/use-get-search-results.ts b/composables/use-get-search-results.ts index 95c09af3..c310d62a 100644 --- a/composables/use-get-search-results.ts +++ b/composables/use-get-search-results.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/vue-query"; -import { z } from "zod"; +import * as v from "valibot"; import { type Entity, useCreateEntity } from "@/composables/use-create-entity"; import type { components, operations } from "@/lib/api-client/api"; @@ -63,39 +63,39 @@ export const logicalOperators = ["and", "or"] as const; export type LogicalOperator = (typeof logicalOperators)[number]; -export const searchFilter = z - .record( - z.enum(categories), - z.object({ - operator: z.enum(operators), - values: z.array(z.union([z.string(), z.number()])), - logicalOperator: z.enum(logicalOperators).default("and"), +export const searchFilter = v.pipe( + v.record( + v.picklist(categories), + v.object({ + operator: v.picklist(operators), + values: v.array(v.union([v.string(), v.number()])), + logicalOperator: v.optional(v.picklist(logicalOperators), "and"), }), - ) - .refine( - (value) => { - return Object.keys(value).length > 0; - }, - { message: "At least one search filter category required" }, - ); - -export const searchFilterString = z - .string() - .transform((value, context) => { + ), + v.check((input) => { + return Object.keys(input).length > 0; + }, "At least one search filter category required"), +); + +export const searchFilterString = v.pipe( + v.string(), + v.transform((input) => { try { // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return JSON.parse(value); + return JSON.parse(input); } catch { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid JSON passed as search filter", - }); - return z.NEVER; + // FIXME: currently not possible, @see https://github.com/fabian-hiller/valibot/issues/182 + // info.issues?.push({ + // message: "Invalid JSON passed as search filter", + // }); + // throw new v.ValiError() + return v.never(); } - }) - .pipe(searchFilter); + }), + searchFilter, +); -export type SearchFilter = z.infer; +export type SearchFilter = v.InferOutput; export interface GetSearchResultsParams extends Omit, "format" | "search"> { diff --git a/composables/use-id-prefix.ts b/composables/use-id-prefix.ts index 9f580016..b47f6db5 100644 --- a/composables/use-id-prefix.ts +++ b/composables/use-id-prefix.ts @@ -3,9 +3,7 @@ import { createUrl } from "@acdh-oeaw/lib"; export function useIdPrefix() { const env = useRuntimeConfig(); - const prefix = String( - createUrl({ baseUrl: env.public.NUXT_PUBLIC_API_BASE_URL, pathname: "/api/entity/" }), - ); + const prefix = String(createUrl({ baseUrl: env.public.apiBaseUrl, pathname: "/api/entity/" })); function getUnprefixedId(id: string) { /** diff --git a/config/project.config.ts b/config/project.config.ts index 26c8bc2e..cdb10e44 100644 --- a/config/project.config.ts +++ b/config/project.config.ts @@ -1,6 +1,6 @@ import { log } from "@acdh-oeaw/lib"; import { ColorSpace, getLuminance, HSL, OKLCH, parse, sRGB, to as convert } from "colorjs.io/fn"; -import { z } from "zod"; +import * as v from "valibot"; import projectConfig from "../project.config.json" assert { type: "json" }; @@ -8,32 +8,32 @@ ColorSpace.register(sRGB); ColorSpace.register(HSL); ColorSpace.register(OKLCH); -const schema = z.object({ - colors: z - .object({ - brand: z.string().min(1), - geojsonPoints: z.string().min(1), - geojsonAreaCenterPoints: z.string().min(1), - entityColors: z.object({ - place: z.string().min(1), - source: z.string().min(1), - person: z.string().min(1), - group: z.string().min(1), - move: z.string().min(1), - event: z.string().min(1), - activity: z.string().min(1), - acquisition: z.string().min(1), - feature: z.string().min(1), - artifact: z.string().min(1), - file: z.string().min(1), - human_remains: z.string().min(1), - stratigraphic_unit: z.string().min(1), - type: z.string().min(1), +const schema = v.object({ + colors: v.pipe( + v.object({ + brand: v.pipe(v.string(), v.nonEmpty()), + disabledNodeColor: v.pipe(v.string(), v.nonEmpty()), + entityColors: v.object({ + acquisition: v.pipe(v.string(), v.nonEmpty()), + activity: v.pipe(v.string(), v.nonEmpty()), + artifact: v.pipe(v.string(), v.nonEmpty()), + event: v.pipe(v.string(), v.nonEmpty()), + feature: v.pipe(v.string(), v.nonEmpty()), + file: v.pipe(v.string(), v.nonEmpty()), + group: v.pipe(v.string(), v.nonEmpty()), + human_remains: v.pipe(v.string(), v.nonEmpty()), + move: v.pipe(v.string(), v.nonEmpty()), + person: v.pipe(v.string(), v.nonEmpty()), + place: v.pipe(v.string(), v.nonEmpty()), + source: v.pipe(v.string(), v.nonEmpty()), + stratigraphic_unit: v.pipe(v.string(), v.nonEmpty()), + type: v.pipe(v.string(), v.nonEmpty()), }), - entityDefaultColor: z.string().min(1), - disabledNodeColor: z.string().min(1), - }) - .transform((values) => { + entityDefaultColor: v.pipe(v.string(), v.nonEmpty()), + geojsonPoints: v.pipe(v.string(), v.nonEmpty()), + geojsonAreaCenterPoints: v.pipe(v.string(), v.nonEmpty()), + }), + v.transform((values) => { const color = parse(values.brand); const luminance = getLuminance(convert(color, OKLCH)); const [h, s, l] = convert(color, HSL).coords; @@ -44,29 +44,31 @@ const schema = z.object({ brandContrast: luminance > 0.5 ? "hsl(0deg 0% 0%)" : "hsl(0deg 0% 100%)", }; }), - fullscreen: z.boolean(), - map: z.object({ - startPage: z.boolean(), + ), + defaultLocale: v.picklist(["de", "en"]), + fullscreen: v.boolean(), + imprint: v.picklist(["acdh-ch", "custom", "none"]), + logos: v.object({ + light: v.string(), + dark: v.string(), + withTextLight: v.string(), + withTextDark: v.string(), }), - defaultLocale: z.enum(["de", "en"]), - logos: z.object({ - light: z.string(), - dark: z.string(), - withTextLight: z.string(), - withTextDark: z.string(), + map: v.object({ + startPage: v.boolean(), }), - imprint: z.enum(["acdh-ch", "custom", "none"]), - twitter: z.string().optional(), + twitter: v.optional(v.string()), }); -const result = schema.safeParse(projectConfig); +const result = v.safeParse(schema, projectConfig); if (!result.success) { const message = "Invalid project configuration."; - log.error(message, result.error.flatten().fieldErrors); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + log.error(message, v.flatten(result.issues).nested); const error = new Error(message); delete error.stack; throw error; } -export const project = result.data; +export const project = result.output; diff --git a/e2e/lib/fixtures/a11y.ts b/e2e/lib/fixtures/a11y.ts new file mode 100644 index 00000000..62915b17 --- /dev/null +++ b/e2e/lib/fixtures/a11y.ts @@ -0,0 +1,24 @@ +import type { Page } from "@playwright/test"; +import type { ElementContext, Result, RunOptions } from "axe-core"; +import { checkA11y, getViolations, injectAxe } from "axe-playwright"; + +export interface AccessibilityScanner { + check: (params?: { selector?: ElementContext; skipFailures?: boolean }) => Promise; + getViolations: (params?: { + options?: RunOptions; + selector?: ElementContext; + }) => Promise>; +} + +export async function createAccessibilityScanner(page: Page): Promise { + await injectAxe(page); + + return { + check(params?: { selector?: ElementContext; skipFailures?: boolean }) { + return checkA11y(page, params?.selector, { detailedReport: true }, params?.skipFailures); + }, + getViolations(params?: { options?: RunOptions; selector?: ElementContext }) { + return getViolations(page, params?.selector, params?.options); + }, + }; +} diff --git a/e2e/lib/fixtures/i18n.ts b/e2e/lib/fixtures/i18n.ts new file mode 100644 index 00000000..5b75a84e --- /dev/null +++ b/e2e/lib/fixtures/i18n.ts @@ -0,0 +1,27 @@ +import type { Page } from "@playwright/test"; +import { createI18n as _createI18n, type I18n as _I18n } from "vue-i18n"; + +import { defaultLocale, type Locale, type Messages } from "@/config/i18n.config"; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type I18n = _I18n<{ [K in Locale]: Messages }, {}, {}, Locale, false>["global"]; + +export async function createI18n(_page: Page, locale = defaultLocale): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const _messages = await import(`@/messages/${locale}/common.json`, { with: { type: "json" } }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const _project = await import(`@/messages/${locale}/project.json`, { with: { type: "json" } }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const messages = { ..._project.default, ..._messages.default } as Messages; + + // @ts-expect-error Only messages for single locale provided. + return _createI18n({ + legacy: false, + locale, + messages: { + [locale]: messages, + }, + }).global; +} + +export type WithI18n = T & { i18n: I18n }; diff --git a/e2e/lib/fixtures/imprint-page.ts b/e2e/lib/fixtures/imprint-page.ts index 55a7bb06..6fd674e2 100644 --- a/e2e/lib/fixtures/imprint-page.ts +++ b/e2e/lib/fixtures/imprint-page.ts @@ -1,19 +1,28 @@ import type { Locator, Page } from "@playwright/test"; import { defaultLocale, type Locale } from "@/config/i18n.config"; +import type { I18n } from "@/e2e/lib/fixtures/i18n"; export class ImprintPage { readonly page: Page; readonly locale: Locale; + readonly i18n: I18n; + readonly url: string; + readonly mainContent: Locator; readonly title: Locator; + readonly skipLink: Locator; - constructor(page: Page, locale = defaultLocale) { + constructor(page: Page, locale = defaultLocale, i18n: I18n) { this.page = page; this.locale = locale; + this.i18n = i18n; + this.url = `/${locale}/imprint`; + this.mainContent = page.getByRole("main"); this.title = page.getByRole("heading", { level: 1 }); + this.skipLink = page.getByRole("link", { name: i18n.t("DefaultLayout.skip-to-main-content") }); } async goto() { - await this.page.goto(`/${this.locale}/imprint`); + return this.page.goto(this.url); } } diff --git a/e2e/lib/fixtures/index-page.ts b/e2e/lib/fixtures/index-page.ts index c2885a13..7c0508c2 100644 --- a/e2e/lib/fixtures/index-page.ts +++ b/e2e/lib/fixtures/index-page.ts @@ -1,19 +1,28 @@ import type { Locator, Page } from "@playwright/test"; import { defaultLocale, type Locale } from "@/config/i18n.config"; +import type { I18n } from "@/e2e/lib/fixtures/i18n"; export class IndexPage { readonly page: Page; readonly locale: Locale; + readonly i18n: I18n; + readonly url: string; + readonly mainContent: Locator; readonly title: Locator; + readonly skipLink: Locator; - constructor(page: Page, locale = defaultLocale) { + constructor(page: Page, locale = defaultLocale, i18n: I18n) { this.page = page; this.locale = locale; + this.i18n = i18n; + this.url = `/${locale}`; + this.mainContent = page.getByRole("main"); this.title = page.getByRole("heading", { level: 1 }); + this.skipLink = page.getByRole("link", { name: i18n.t("DefaultLayout.skip-to-main-content") }); } async goto() { - await this.page.goto(`/${this.locale}`); + return this.page.goto(this.url); } } diff --git a/e2e/lib/test.ts b/e2e/lib/test.ts index 2bc89a7c..3c3bb8b8 100644 --- a/e2e/lib/test.ts +++ b/e2e/lib/test.ts @@ -1,83 +1,46 @@ import { test as base } from "@playwright/test"; -import type { ElementContext, Result, RunOptions } from "axe-core"; -import { checkA11y, getViolations, injectAxe } from "axe-playwright"; -import { createI18n as _createI18n, type I18n } from "vue-i18n"; -import { defaultLocale, type Locale, type Messages } from "@/config/i18n.config"; +import { defaultLocale, type Locale } from "@/config/i18n.config"; +import { type AccessibilityScanner, createAccessibilityScanner } from "@/e2e/lib/fixtures/a11y"; +import { createI18n, type I18n, type WithI18n } from "@/e2e/lib/fixtures/i18n"; import { ImprintPage } from "@/e2e/lib/fixtures/imprint-page"; import { IndexPage } from "@/e2e/lib/fixtures/index-page"; interface Fixtures { - createAccessibilityScanner: () => Promise<{ - check: (params?: { selector?: ElementContext; skipFailures?: boolean }) => Promise; - getViolations: (params?: { - options?: RunOptions; - selector?: ElementContext; - }) => Promise>; - }>; - createI18n: ( - locale?: Locale, - // eslint-disable-next-line @typescript-eslint/ban-types - ) => Promise["global"]>; - createImprintPage: (locale: Locale) => ImprintPage; - createIndexPage: (locale: Locale) => IndexPage; + createAccessibilityScanner: () => Promise; + createI18n: (locale: Locale) => Promise; + createImprintPage: (locale: Locale) => Promise>; + createIndexPage: (locale: Locale) => Promise>; } export const test = base.extend({ async createAccessibilityScanner({ page }, use) { - async function createAccessibilityScanner() { - await injectAxe(page); - - return { - check(params?: { selector?: ElementContext; skipFailures?: boolean }) { - return checkA11y(page, params?.selector, { detailedReport: true }, params?.skipFailures); - }, - getViolations(params?: { options?: RunOptions; selector?: ElementContext }) { - return getViolations(page, params?.selector, params?.options); - }, - }; - } - - await use(createAccessibilityScanner); + await use(() => { + return createAccessibilityScanner(page); + }); }, - async createI18n({ page: _ }, use) { - async function createI18n(locale = defaultLocale) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const _messages = await import(`@/messages/${locale}/common.json`, { - with: { type: "json" }, - }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const _project = await import(`@/messages/${locale}/project.json`, { - with: { type: "json" }, - }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const messages = { ..._messages.default, ..._project.default } as Messages; - - return _createI18n({ - legacy: false, - locale, - messages: { - [locale]: messages, - }, - }).global; - } - - // @ts-expect-error Only messages for single locale provided. - await use(createI18n); + async createI18n({ page }, use) { + await use((locale = defaultLocale) => { + return createI18n(page, locale); + }); }, async createImprintPage({ page }, use) { - function createImprintPage(locale = defaultLocale) { - return new ImprintPage(page, locale); + async function createImprintPage(locale = defaultLocale) { + const i18n = await createI18n(page, locale); + const imprintPage = new ImprintPage(page, locale, i18n); + return { i18n, imprintPage }; } await use(createImprintPage); }, async createIndexPage({ page }, use) { - function createIndexPage(locale = defaultLocale) { - return new IndexPage(page, locale); + async function createIndexPage(locale = defaultLocale) { + const i18n = await createI18n(page, locale); + const indexPage = new IndexPage(page, locale, i18n); + return { i18n, indexPage }; } await use(createIndexPage); diff --git a/e2e/tests/app/analytics.test.ts b/e2e/tests/app/analytics.test.ts index 4596f5bd..7385bb3a 100644 --- a/e2e/tests/app/analytics.test.ts +++ b/e2e/tests/app/analytics.test.ts @@ -8,14 +8,16 @@ if (process.env.NUXT_PUBLIC_MATOMO_BASE_URL && process.env.NUXT_PUBLIC_MATOMO_ID ); test.describe("analytics service", () => { - test("should track page views", async ({ page }) => { + test("should track page views", async ({ createIndexPage }) => { + const { indexPage, i18n } = await createIndexPage(defaultLocale); + const { page } = indexPage; const initialResponsePromise = page.waitForResponse(baseUrl); - await page.goto("/en"); + await indexPage.goto(); const initialResponse = await initialResponsePromise; expect(initialResponse.status()).toBe(204); const responsePromise = page.waitForResponse(baseUrl); - await page.getByRole("link", { name: "Imprint" }).click(); + await page.getByRole("link", { name: i18n.t("AppFooter.links.imprint") }).click(); const response = await responsePromise; expect(response.status()).toBe(204); }); diff --git a/e2e/tests/app/app.test.ts b/e2e/tests/app/app.test.ts index 67435d01..f253df83 100644 --- a/e2e/tests/app/app.test.ts +++ b/e2e/tests/app/app.test.ts @@ -1,6 +1,6 @@ import { createUrl } from "@acdh-oeaw/lib"; -import { locales } from "@/config/i18n.config"; +import { defaultLocale, locales } from "@/config/i18n.config"; import { expect, test } from "@/e2e/lib/test"; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -8,7 +8,7 @@ const baseUrl = process.env.NUXT_PUBLIC_APP_BASE_URL!; test.describe("app", () => { if (process.env.NUXT_PUBLIC_BOTS !== "enabled") { - test("serves a robots.txt which disallows search engine bots", async ({ request }) => { + test("should serve a robots.txt which disallows search engine bots", async ({ request }) => { const response = await request.get("/robots.txt"); const body = await response.body(); @@ -17,7 +17,7 @@ test.describe("app", () => { ); }); } else { - test("serves a robots.txt", async ({ request }) => { + test("should serve a robots.txt", async ({ request }) => { const response = await request.get("/robots.txt"); const body = await response.body(); @@ -32,7 +32,7 @@ test.describe("app", () => { }); } - test("serves a sitemap.xml", async ({ request }) => { + test("should serve a sitemap.xml", async ({ request }) => { const response = await request.get("/sitemap.xml"); const body = await response.body(); @@ -58,17 +58,17 @@ test.describe("app", () => { } }); - test("serves a webmanifest", async ({ request }) => { + test("should serve a webmanifest", async ({ createI18n, request }) => { const response = await request.get("/manifest.webmanifest"); const body = await response.body(); - // TODO: use toMatchSnapshot + const i18n = await createI18n(defaultLocale); + expect(body.toString()).toEqual( JSON.stringify({ - name: "OpenAtlas Discovery", - short_name: "OpenAtlas Discovery", - description: - "OpenAtlas is an open source database software developed especially to acquire, edit and manage research data from various fields of humanities.", + name: i18n.t("Metadata.name"), + short_name: i18n.t("Metadata.shortName"), + description: i18n.t("Metadata.description"), start_url: "/", display: "standalone", background_color: "#fff", @@ -83,51 +83,73 @@ test.describe("app", () => { ); }); - test("serves a favicon.ico", async ({ request }) => { + test("should serve a favicon.ico", async ({ request }) => { const response = await request.get("/favicon.ico"); const status = response.status(); expect(status).toEqual(200); }); - test("serves an svg favicon", async ({ request }) => { + test("should serve an svg favicon", async ({ request }) => { const response = await request.get("/icon.svg"); const status = response.status(); expect(status).toEqual(200); }); - test("serves an apple favicon", async ({ request }) => { + test("should serve an apple favicon", async ({ request }) => { const response = await request.get("/apple-icon.png"); const status = response.status(); expect(status).toEqual(200); }); - test.describe("sets color mode according to system preference", () => { + test.describe("should set color mode according to system preference", () => { test.use({ colorScheme: "no-preference" }); - test("with no preference", async ({ page }) => { - await page.goto("/en"); - await expect(page.locator("html")).toHaveAttribute("data-ui-color-scheme", "light"); + test("with no preference", async ({ createIndexPage }) => { + const { indexPage } = await createIndexPage(defaultLocale); + await indexPage.goto(); + await expect(indexPage.page.locator("html")).toHaveAttribute("data-ui-color-scheme", "light"); }); }); - test.describe("sets color mode according to system preference", () => { + test.describe("should set color mode according to system preference", () => { test.use({ colorScheme: "light" }); - test("in light mode", async ({ page }) => { - await page.goto("/en"); - await expect(page.locator("html")).toHaveAttribute("data-ui-color-scheme", "light"); + test("in light mode", async ({ createIndexPage }) => { + const { indexPage } = await createIndexPage(defaultLocale); + await indexPage.goto(); + await expect(indexPage.page.locator("html")).toHaveAttribute("data-ui-color-scheme", "light"); }); }); - test.describe("sets color mode according to system preference", () => { + test.describe("should set color mode according to system preference", () => { test.use({ colorScheme: "dark" }); - test("in dark mode", async ({ page }) => { - await page.goto("/en"); - await expect(page.locator("html")).toHaveAttribute("data-ui-color-scheme", "dark"); + test("in dark mode", async ({ createIndexPage }) => { + const { indexPage } = await createIndexPage(defaultLocale); + await indexPage.goto(); + await expect(indexPage.page.locator("html")).toHaveAttribute("data-ui-color-scheme", "dark"); }); }); + + test("should skip to main content with skip-link", async ({ createIndexPage }) => { + const { indexPage } = await createIndexPage(defaultLocale); + await indexPage.goto(); + + await indexPage.page.keyboard.press("Tab"); + await expect(indexPage.skipLink).toBeFocused(); + + await indexPage.skipLink.click(); + await expect(indexPage.mainContent).toBeFocused(); + }); + + test("should set `lang` attribute on `html` element", async ({ createIndexPage }) => { + for (const locale of locales) { + const { indexPage } = await createIndexPage(locale); + await indexPage.goto(); + await expect(indexPage.page.locator("html")).toHaveAttribute("lang", locale); + } + }); }); diff --git a/e2e/tests/app/i18n.test.ts b/e2e/tests/app/i18n.test.ts index d736bace..34b1b458 100644 --- a/e2e/tests/app/i18n.test.ts +++ b/e2e/tests/app/i18n.test.ts @@ -7,7 +7,7 @@ import { expect, test } from "@/e2e/lib/test"; const baseUrl = process.env.NUXT_PUBLIC_APP_BASE_URL!; test.describe("i18n", () => { - test.describe("redirects root route to preferred locale", () => { + test.describe("should redirect root route to preferred locale", () => { test.use({ locale: "en" }); test("with default locale", async ({ page }) => { @@ -16,7 +16,7 @@ test.describe("i18n", () => { }); }); - test.describe("redirects root route to preferred locale", () => { + test.describe("should redirect root route to preferred locale", () => { test.use({ locale: "de" }); /** @@ -29,7 +29,7 @@ test.describe("i18n", () => { }); }); - test.describe("redirects root route to preferred locale", () => { + test.describe("should redirect root route to preferred locale", () => { test.use({ locale: "fr" }); test("with unsupported locale", async ({ page }) => { @@ -38,39 +38,47 @@ test.describe("i18n", () => { }); }); - test("displays not-found page for unknown locale", async ({ page }) => { + test("should display not-found page for unknown locale", async ({ createI18n, page }) => { + const i18n = await createI18n("en"); const response = await page.goto("/unknown"); expect(response?.status()).toBe(404); - await expect(page.getByRole("heading", { name: "Page not found" })).toBeVisible(); + await expect(page.getByRole("heading", { name: i18n.t("NotFoundPage.title") })).toBeVisible(); }); - test("displays localised not-found page for unknown pathname", async ({ page }) => { + test("should display localised not-found page for unknown pathname", async ({ + createI18n, + page, + }) => { + const i18n = await createI18n("de"); const response = await page.goto("/de/unknown"); expect(response?.status()).toBe(404); - await expect(page.getByRole("heading", { name: "Seite nicht gefunden" })).toBeVisible(); + await expect(page.getByRole("heading", { name: i18n.t("NotFoundPage.title") })).toBeVisible(); }); - test("supports switching locale", async ({ page }) => { - await page.goto("/de/imprint"); + test("should support switching locale", async ({ createImprintPage, createI18n, page }) => { + const { imprintPage, i18n: i18nDe } = await createImprintPage("de"); + await imprintPage.goto(); + await expect(page).toHaveURL("/de/imprint"); - await expect(page.getByRole("heading", { name: "Impressum" })).toBeVisible(); - await expect(page).toHaveTitle("Impressum | OpenAtlas Discovery"); + await expect(page.getByRole("heading", { name: i18nDe.t("ImprintPage.title") })).toBeVisible(); + await expect(page).toHaveTitle( + [i18nDe.t("ImprintPage.meta.title"), i18nDe.t("Metadata.name")].join(" | "), + ); - await page.getByRole("link", { name: "Zu Englisch wechseln" }).click(); + await page + .getByRole("link", { name: i18nDe.t("LocaleSwitcher.switch-locale", { locale: "Englisch" }) }) + .click(); - await expect(page).toHaveURL("/en/imprint"); - await expect(page.getByRole("heading", { name: "Imprint" })).toBeVisible(); - await expect(page).toHaveTitle("Imprint | OpenAtlas Discovery"); - }); + const i18nEn = await createI18n("en"); - test("sets `lang` attribute on `html` element", async ({ page }) => { - for (const locale of locales) { - await page.goto(`/${locale}`); - await expect(page.locator("html")).toHaveAttribute("lang", locale); - } + await expect(page).toHaveURL("/en/imprint"); + await expect(page.getByRole("heading", { name: i18nEn.t("ImprintPage.title") })).toBeVisible(); + await expect(page).toHaveTitle( + [i18nEn.t("ImprintPage.meta.title"), i18nEn.t("Metadata.name")].join(" | "), + ); }); - test("sets alternate links in link tags", async ({ page }) => { + test("should set alternate links in link tags", async ({ page }) => { function createAbsoluteUrl(pathname: string) { return String(createUrl({ baseUrl, pathname })); } @@ -87,18 +95,13 @@ test.describe("i18n", () => { }); }); - // TODO: use toMatchSnapshot - expect(links).toEqual([ - ``, - ``, - ``, - ]); + expect(links).toEqual( + expect.arrayContaining([ + ``, + ``, + ``, + ]), + ); } } }); diff --git a/e2e/tests/app/metadata.test.ts b/e2e/tests/app/metadata.test.ts index 863cbf31..2663661c 100644 --- a/e2e/tests/app/metadata.test.ts +++ b/e2e/tests/app/metadata.test.ts @@ -2,15 +2,17 @@ import { createUrl } from "@acdh-oeaw/lib"; import { locales } from "@/config/i18n.config"; import { expect, test } from "@/e2e/lib/test"; +import { escape } from "@/utils/safe-json-ld-replacer"; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const baseUrl = process.env.NUXT_PUBLIC_APP_BASE_URL!; -test("sets a canonical url", async ({ page }) => { +test("should set a canonical url", async ({ createIndexPage }) => { for (const locale of locales) { - await page.goto(`/${locale}`); + const { indexPage } = await createIndexPage(locale); + await indexPage.goto(); - const canonicalUrl = page.locator('link[rel="canonical"]'); + const canonicalUrl = indexPage.page.locator('link[rel="canonical"]'); await expect(canonicalUrl).toHaveAttribute( "href", String(createUrl({ baseUrl, pathname: `/${locale}` })), @@ -18,15 +20,21 @@ test("sets a canonical url", async ({ page }) => { } }); -test("sets document title on not-found page", async ({ page }) => { +test("should set document title on not-found page", async ({ createI18n, page }) => { + const i18nEn = await createI18n("en"); await page.goto("/unknown"); - await expect(page).toHaveTitle("Page not found | OpenAtlas Discovery"); + await expect(page).toHaveTitle( + [i18nEn.t("NotFoundPage.meta.title"), i18nEn.t("Metadata.name")].join(" | "), + ); + const i18nDe = await createI18n("de"); await page.goto("/de/unknown"); - await expect(page).toHaveTitle("Seite nicht gefunden | OpenAtlas Discovery"); + await expect(page).toHaveTitle( + [i18nDe.t("NotFoundPage.meta.title"), i18nDe.t("Metadata.name")].join(" | "), + ); }); -test("disallows indexing of not-found page", async ({ page }) => { +test("should disallow indexing of not-found page", async ({ page }) => { for (const pathname of ["/unknown", "/de/unknown"]) { await page.goto(pathname); @@ -35,9 +43,14 @@ test("disallows indexing of not-found page", async ({ page }) => { } }); -test.describe("sets page metadata", () => { - test("static", async ({ page }) => { - await page.goto("/en"); +test("should set page metadata", async ({ createIndexPage }) => { + for (const locale of locales) { + const { indexPage, i18n } = await createIndexPage(locale); + await indexPage.goto(); + const { page } = indexPage; + + expect(i18n.t("Metadata.name")).toBeTruthy(); + expect(i18n.t("Metadata.description")).toBeTruthy(); const ogType = page.locator('meta[property="og:type"]'); await expect(ogType).toHaveAttribute("content", "website"); @@ -45,110 +58,79 @@ test.describe("sets page metadata", () => { const twCard = page.locator('meta[name="twitter:card"]'); await expect(twCard).toHaveAttribute("content", "summary_large_image"); - const twCreator = page.locator('meta[name="twitter:creator"]'); - await expect(twCreator).toHaveAttribute("content", "@openatlas_eu"); + // const twCreator = page.locator('meta[name="twitter:creator"]'); + // await expect(twCreator).toHaveAttribute("content", project.twitter); - const twSite = page.locator('meta[name="twitter:site"]'); - await expect(twSite).toHaveAttribute("content", "@openatlas_eu"); + // const twSite = page.locator('meta[name="twitter:site"]'); + // await expect(twSite).toHaveAttribute("content", project.twitter); // const googleSiteVerification = page.locator('meta[name="google-site-verification"]'); // await expect(googleSiteVerification).toHaveAttribute("content", ""); - }); - - test("with en locale", async ({ page }) => { - await page.goto("/en"); - await expect(page).toHaveTitle("Home | OpenAtlas Discovery"); + await expect(page).toHaveTitle( + [i18n.t("IndexPage.meta.title"), i18n.t("Metadata.name")].join(" | "), + ); const metaDescription = page.locator('meta[name="description"]'); - await expect(metaDescription).toHaveAttribute( - "content", - "OpenAtlas is an open source database software developed especially to acquire, edit and manage research data from various fields of humanities.", - ); + await expect(metaDescription).toHaveAttribute("content", i18n.t("Metadata.description")); const ogTitle = page.locator('meta[property="og:title"]'); - await expect(ogTitle).toHaveAttribute("content", "Home"); + await expect(ogTitle).toHaveAttribute("content", i18n.t("IndexPage.meta.title")); const ogDescription = page.locator('meta[property="og:description"]'); - await expect(ogDescription).toHaveAttribute( - "content", - "OpenAtlas is an open source database software developed especially to acquire, edit and manage research data from various fields of humanities.", - ); + await expect(ogDescription).toHaveAttribute("content", i18n.t("Metadata.description")); const ogUrl = page.locator('meta[property="og:url"]'); - await expect(ogUrl).toHaveAttribute("content", String(createUrl({ baseUrl, pathname: "/en" }))); - - const ogLocale = page.locator('meta[property="og:locale"]'); - await expect(ogLocale).toHaveAttribute("content", "en"); - }); - - test("with de locale", async ({ page }) => { - await page.goto("/de"); - - await expect(page).toHaveTitle("Startseite | OpenAtlas Discovery"); - - const metaDescription = page.locator('meta[name="description"]'); - await expect(metaDescription).toHaveAttribute( - "content", - "OpenAtlas ist eine Open-Source-Datenbanksoftware, die speziell für die Erfassung, Bearbeitung und Verwaltung von Forschungsdaten aus verschiedenen Bereichen der Geisteswissenschaften entwickelt wurde.", - ); - - const ogTitle = page.locator('meta[property="og:title"]'); - await expect(ogTitle).toHaveAttribute("content", "Startseite"); - - const ogDescription = page.locator('meta[property="og:description"]'); - await expect(ogDescription).toHaveAttribute( + await expect(ogUrl).toHaveAttribute( "content", - "OpenAtlas ist eine Open-Source-Datenbanksoftware, die speziell für die Erfassung, Bearbeitung und Verwaltung von Forschungsdaten aus verschiedenen Bereichen der Geisteswissenschaften entwickelt wurde.", + String(createUrl({ baseUrl, pathname: `/${locale}` })), ); - const ogUrl = page.locator('meta[property="og:url"]'); - await expect(ogUrl).toHaveAttribute("content", String(createUrl({ baseUrl, pathname: "/de" }))); - const ogLocale = page.locator('meta[property="og:locale"]'); - await expect(ogLocale).toHaveAttribute("content", "de"); - }); + await expect(ogLocale).toHaveAttribute("content", locale); + } }); -test.describe("adds json+ld metadata", () => { - test("with en locale", async ({ page }) => { - await page.goto("/en"); - - const metadata = await page.locator('script[type="application/ld+json"]').textContent(); - expect(metadata).toBe( - JSON.stringify({ - "@context": "https://schema.org", - "@type": "WebSite", - name: "OpenAtlas Discovery", - description: - "OpenAtlas is an open source database software developed especially to acquire, edit and manage research data from various fields of humanities.", - }), - ); - }); +test("should add json+ld metadata", async ({ createIndexPage }) => { + for (const locale of locales) { + const { indexPage, i18n } = await createIndexPage(locale); + await indexPage.goto(); - test("with de locale", async ({ page }) => { - await page.goto("/de"); + const metadata = await indexPage.page + .locator('script[type="application/ld+json"]') + .textContent(); - const metadata = await page.locator('script[type="application/ld+json"]').textContent(); + // eslint-disable-next-line playwright/prefer-web-first-assertions expect(metadata).toBe( JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", - name: "OpenAtlas Discovery", - description: - "OpenAtlas ist eine Open-Source-Datenbanksoftware, die speziell für die Erfassung, Bearbeitung und Verwaltung von Forschungsdaten aus verschiedenen Bereichen der Geisteswissenschaften entwickelt wurde.", + name: escape(i18n.t("Metadata.name")), + description: escape(i18n.t("Metadata.description")), }), ); - }); + } }); -test("serves an open-graph image", async ({ request }) => { - for (const _locale of locales) { +test("should serve an open-graph image", async ({ createIndexPage, request }) => { + for (const locale of locales) { // FIXME: serve og image per locale - // const response = await request.get(`/${locale}/opengraph-image.png`); - const response = await request.get("opengraph-image.png"); + // const imagePath = `/${locale}/opengraph-image.png`; + const imagePath = "/opengraph-image.png"; + + const { indexPage } = await createIndexPage(locale); + await indexPage.goto(); + + await expect(indexPage.page.locator('meta[property="og:image"]')).toHaveAttribute( + "content", + expect.stringContaining(String(createUrl({ baseUrl, pathname: imagePath }))), + ); + + const response = await request.get(imagePath); const status = response.status(); + const contentType = response.headers()["content-type"]; - expect(status).toEqual(200); + expect(status).toBe(200); + expect(contentType).toBe("image/png"); } }); diff --git a/e2e/tests/pages/imprint.test.ts b/e2e/tests/pages/imprint.test.ts index c6adaaa9..0c686c0f 100644 --- a/e2e/tests/pages/imprint.test.ts +++ b/e2e/tests/pages/imprint.test.ts @@ -2,14 +2,13 @@ import { locales } from "@/config/i18n.config"; import { expect, test } from "@/e2e/lib/test"; test.describe("imprint page", () => { - test("should have document title", async ({ createI18n, createImprintPage }) => { + test("should have document title", async ({ createImprintPage }) => { for (const locale of locales) { - const { t } = await createI18n(locale); - const imprintPage = createImprintPage(locale); + const { i18n, imprintPage } = await createImprintPage(locale); await imprintPage.goto(); await expect(imprintPage.page).toHaveTitle( - [t("ImprintPage.meta.title"), t("Metadata.name")].join(" | "), + [i18n.t("ImprintPage.meta.title"), i18n.t("Metadata.name")].join(" | "), ); } }); @@ -21,19 +20,19 @@ test.describe("imprint page", () => { }; for (const locale of locales) { - const imprintPage = createImprintPage(locale); + const { imprintPage } = await createImprintPage(locale); await imprintPage.goto(); await expect(imprintPage.page.getByRole("main")).toContainText(imprints[locale]); } }); - test.skip("should not have any automatically detectable accessibility issues", async ({ + test("should not have any automatically detectable accessibility issues", async ({ createAccessibilityScanner, createImprintPage, }) => { for (const locale of locales) { - const imprintPage = createImprintPage(locale); + const { imprintPage } = await createImprintPage(locale); await imprintPage.goto(); const { getViolations } = await createAccessibilityScanner(); @@ -43,7 +42,7 @@ test.describe("imprint page", () => { test.skip("should not have visible changes", async ({ createImprintPage }) => { for (const locale of locales) { - const imprintPage = createImprintPage(locale); + const { imprintPage } = await createImprintPage(locale); await imprintPage.goto(); await expect(imprintPage.page).toHaveScreenshot(); diff --git a/e2e/tests/pages/index.test.ts b/e2e/tests/pages/index.test.ts index f64a2102..aae89cc5 100644 --- a/e2e/tests/pages/index.test.ts +++ b/e2e/tests/pages/index.test.ts @@ -2,24 +2,23 @@ import { locales } from "@/config/i18n.config"; import { expect, test } from "@/e2e/lib/test"; test.describe("index page", () => { - test("should have document title", async ({ createI18n, createIndexPage }) => { + test("should have document title", async ({ createIndexPage }) => { for (const locale of locales) { - const { t } = await createI18n(locale); - const indexPage = createIndexPage(locale); + const { i18n, indexPage } = await createIndexPage(locale); await indexPage.goto(); await expect(indexPage.page).toHaveTitle( - [t("IndexPage.meta.title"), t("Metadata.name")].join(" | "), + [i18n.t("IndexPage.meta.title"), i18n.t("Metadata.name")].join(" | "), ); } }); - test.skip("should not have any automatically detectable accessibility issues", async ({ + test("should not have any automatically detectable accessibility issues", async ({ createAccessibilityScanner, createIndexPage, }) => { for (const locale of locales) { - const indexPage = createIndexPage(locale); + const { indexPage } = await createIndexPage(locale); await indexPage.goto(); const { getViolations } = await createAccessibilityScanner(); @@ -29,7 +28,7 @@ test.describe("index page", () => { test.skip("should not have visible changes", async ({ createIndexPage }) => { for (const locale of locales) { - const indexPage = createIndexPage(locale); + const { indexPage } = await createIndexPage(locale); await indexPage.goto(); await expect(indexPage.page).toHaveScreenshot(); diff --git a/layouts/default.vue b/layouts/default.vue index 95b48be6..1e392d66 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -9,6 +9,8 @@ const env = useRuntimeConfig(); const locale = useLocale(); const t = useTranslations(); +const router = useRouter(); + const i18nHead = useLocaleHead({ addDirAttribute: true, identifierAttribute: "id", @@ -49,7 +51,7 @@ useHead({ property: "og:image", content: String( createUrl({ - baseUrl: env.public.NUXT_PUBLIC_APP_BASE_URL, + baseUrl: env.public.appBaseUrl, pathname: "/opengraph-image.png", }), ), @@ -76,17 +78,14 @@ useHead({ { type: "application/ld+json", innerHTML: JSON.stringify(jsonLd, safeJsonLdReplacer) }, ]; - if ( - isNonEmptyString(env.public.NUXT_PUBLIC_MATOMO_BASE_URL) && - isNonEmptyString(env.public.NUXT_PUBLIC_MATOMO_ID) - ) { - const baseUrl = env.public.NUXT_PUBLIC_MATOMO_BASE_URL; + if (isNonEmptyString(env.public.matomoBaseUrl) && isNonEmptyString(env.public.matomoId)) { + const baseUrl = env.public.matomoBaseUrl; scripts.push({ type: "", innerHTML: createAnalyticsScript( baseUrl.endsWith("/") ? baseUrl : baseUrl + "/", - env.public.NUXT_PUBLIC_MATOMO_ID, + env.public.matomoId, ), }); } @@ -95,6 +94,10 @@ useHead({ }), }); +router.afterEach((to, from) => { + trackPageView(to, from); +}); + const fullscreen = "--container-width: ;"; const container = "--container-width: 1536px;"; @@ -113,6 +116,5 @@ const container = "--container-width: 1536px;"; - diff --git a/nuxt.config.ts b/nuxt.config.ts index fb90f5c4..e40c7a89 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -27,7 +27,7 @@ export default defineNuxtConfig({ }, css: ["@fontsource-variable/inter/slnt.css", "tailwindcss/tailwind.css", "@/styles/index.css"], devtools: { - enabled: process.env.NODE_ENV === "development", + enabled: true, }, experimental: { componentIslands: { @@ -47,6 +47,9 @@ export default defineNuxtConfig({ /** @see https://github.com/nuxt/nuxt/issues/21821 */ inlineStyles: false, }, + future: { + compatibilityVersion: 4, + }, i18n: { baseUrl, defaultLocale, @@ -59,9 +62,6 @@ export default defineNuxtConfig({ strategy: "prefix", vueI18n: "./i18n.config.ts", }, - image: { - domains: [], - }, imports: { dirs: ["./config/"], }, @@ -70,7 +70,6 @@ export default defineNuxtConfig({ compressPublicAssets: true, prerender: { routes: ["/manifest.webmanifest", "/robots.txt", "/sitemap.xml"], - failOnError: false, }, }, postcss: { @@ -81,19 +80,20 @@ export default defineNuxtConfig({ runtimeConfig: { NODE_ENV: process.env.NODE_ENV, public: { - NUXT_PUBLIC_API_BASE_URL: process.env.NUXT_PUBLIC_API_BASE_URL, - NUXT_PUBLIC_APP_BASE_URL: process.env.NUXT_PUBLIC_APP_BASE_URL, - NUXT_PUBLIC_BOTS: process.env.NUXT_PUBLIC_BOTS, - NUXT_PUBLIC_DATABASE: process.env.NUXT_PUBLIC_DATABASE, - NUXT_PUBLIC_GIT_BRANCH_NAME: branchName, - NUXT_PUBLIC_GIT_COMMIT_HASH: commitHash, - NUXT_PUBLIC_GIT_TAG: tag, - NUXT_PUBLIC_MAP_BASELAYER_URL_DARK: process.env.NUXT_PUBLIC_MAP_BASELAYER_URL_DARK, - NUXT_PUBLIC_MAP_BASELAYER_URL_LIGHT: process.env.NUXT_PUBLIC_MAP_BASELAYER_URL_LIGHT, - NUXT_PUBLIC_MATOMO_BASE_URL: process.env.NUXT_PUBLIC_MATOMO_BASE_URL, - NUXT_PUBLIC_MATOMO_ID: process.env.NUXT_PUBLIC_MATOMO_ID, - NUXT_PUBLIC_OPENAPI_BASE_URL: process.env.NUXT_PUBLIC_OPENAPI_BASE_URL, - NUXT_PUBLIC_REDMINE_ID: process.env.NUXT_PUBLIC_REDMINE_ID, + apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL, + appBaseUrl: process.env.NUXT_PUBLIC_APP_BASE_URL, + bots: process.env.NUXT_PUBLIC_BOTS, + database: process.env.NUXT_PUBLIC_DATABASE, + gitBranchName: branchName, + gitCommitHash: commitHash, + gitTag: tag, + googleSiteVerification: process.env.NUXT_PUBLIC_GOOGLE_SITE_VERIFICATION, + mapBaselayerUrlDark: process.env.NUXT_PUBLIC_MAP_BASELAYER_URL_DARK, + mapBaselayerUrlLight: process.env.NUXT_PUBLIC_MAP_BASELAYER_URL_LIGHT, + matomoBaseUrl: process.env.NUXT_PUBLIC_MATOMO_BASE_URL, + matomoId: process.env.NUXT_PUBLIC_MATOMO_ID, + openapiBaseUrl: process.env.NUXT_PUBLIC_OPENAPI_BASE_URL, + redmineId: process.env.NUXT_PUBLIC_REDMINE_ID, }, }, typescript: { diff --git a/package.json b/package.json index fc0cf272..04268d3e 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "scripts": { "analyze": "nuxt analyze", "build": "nuxt build --dotenv ./.env.local", - "generate": "pnpm generate:server && pnpm generate:reroute-index", - "generate:server": "nuxt generate --dotenv ./.env.local", + "generate": "run-s generate:server generate:reroute-index", + "generate:api-client": "dotenv -c -- tsx ./scripts/generate-api-client.ts", "generate:reroute-index": "tsx ./scripts/generate-reroute-index.ts", + "generate:server": "nuxt generate --dotenv ./.env.local", "dev": "nuxt dev --dotenv ./.env.local", "dev:cms": "decap-server", "format:check": "prettier . \"!./content/**\" --cache --check --ignore-path ./.gitignore", "format:fix": "pnpm run format:check --write", - "generate:api-client": "dotenv -c -- tsx ./scripts/generate-api-client.ts", "lint:check": "run-p --continue-on-error \"lint:*:check\"", "lint:fix": "run-p --continue-on-error \"lint:*:fix\"", "lint:code:check": "eslint . --cache --ext .js,.ts,.vue --ignore-path ./.gitignore", @@ -34,92 +34,92 @@ "test": "exit 0", "test:e2e": "playwright test", "test:e2e:codegen": "playwright codegen", - "test:e2e:update-snapshots": "playwright test --update-snapshots", + "test:e2e:install": "playwright install --with-deps", "test:e2e:ui": "playwright test --ui", + "test:e2e:update-snapshots": "playwright test --update-snapshots", "types:check": "nuxt typecheck", - "validate": "run-p format:check lint:check types:check test test:e2e" + "validate": "run-s format:check lint:check types:check test test:e2e" }, "dependencies": { - "@acdh-oeaw/lib": "^0.1.7", - "@acdh-oeaw/tsconfig": "^1.0.2", - "@fontsource-variable/inter": "^5.0.17", - "@nuxt/content": "^2.12.1", - "@nuxt/image": "^1.4.0", - "@nuxtjs/color-mode": "^3.3.2", - "@nuxtjs/i18n": "^8.2.0", - "@nuxtjs/mdc": "^0.6.1", + "@acdh-oeaw/lib": "^0.1.12", + "@fontsource-variable/inter": "^5.0.18", + "@nuxt/content": "^2.13.0", + "@nuxt/image": "^1.7.0", + "@nuxtjs/color-mode": "^3.4.1", + "@nuxtjs/i18n": "^8.3.1", + "@nuxtjs/mdc": "^0.8.2", "@stefanprobst/netlify-cms-oauth-client": "^0.4.0", "@stefanprobst/openapi-client": "^0.0.3", - "@tanstack/vue-query": "^5.28.4", - "@tanstack/vue-table": "^8.13.2", + "@tanstack/vue-query": "^5.45.0", + "@tanstack/vue-table": "^8.17.3", "@turf/turf": "^7.0.0", - "@vee-validate/zod": "^4.12.6", - "@vueuse/core": "^10.9.0", - "@vueuse/nuxt": "^10.9.0", + "@vee-validate/zod": "^4.13.1", + "@vueuse/core": "^10.11.0", + "@vueuse/nuxt": "^10.11.0", "class-variance-authority": "^0.7.0", "colorjs.io": "^0.5.0", "cva": "^1.0.0-beta.1", - "dotenv": "^16.4.5", - "dotenv-cli": "^7.4.1", - "dotenv-expand": "^11.0.6", - "embla-carousel": "^8.0.0", - "embla-carousel-vue": "^8.0.0", + "embla-carousel": "^8.1.5", + "embla-carousel-vue": "^8.1.5", "fast-glob": "^3.3.2", "graphology": "^0.25.4", "graphology-layout": "^0.6.1", - "graphology-layout-force": "^0.2.4", "graphology-layout-forceatlas2": "^0.10.1", - "graphology-layout-noverlap": "^0.4.2", - "is-ci": "^3.0.1", - "lucide-vue-next": "^0.358.0", - "maplibre-gl": "^4.1.1", - "mirador": "4.0.0-alpha.2", - "npm-run-all2": "^6.1.2", - "nuxt": "^3.11.0", - "openapi-typescript": "^7.0.0-next.8", - "pino-http": "^9.0.0", - "radix-vue": "^1.5.2", - "sigma": "3.0.0-beta.17", - "tailwind-merge": "^2.2.2", - "tify": "^0.30.2", - "tsx": "^4.7.1", - "vee-validate": "^4.12.6", - "vue": "^3.4.21", - "vue-i18n": "^9.10.2", + "lucide-vue-next": "^0.396.0", + "maplibre-gl": "^4.4.1", + "mirador": "^4.0.0-alpha.2", + "nuxt": "^3.12.2", + "openapi-typescript": "^7.0.0", + "pino-http": "^10.1.0", + "radix-vue": "^1.8.4", + "satori": "^0.10.13", + "sigma": "^3.0.0-beta.21", + "tailwind-merge": "^2.3.0", + "valibot": "^0.33.3", + "vee-validate": "^4.13.1", + "vue": "^3.4.29", + "vue-i18n": "^9.13.1", "vue-i18n-routing": "^1.2.0", - "vue-router": "^4.3.0", - "vue-sonner": "^1.1.2", - "zod": "^3.22.4" + "vue-router": "^4.4.0", + "vue-sonner": "^1.1.3", + "zod": "^3.23.8" }, "devDependencies": { - "@acdh-oeaw/eslint-config": "^1.0.7", - "@acdh-oeaw/eslint-config-nuxt": "^1.0.13", - "@acdh-oeaw/eslint-config-vue": "^1.0.12", + "@acdh-oeaw/eslint-config": "^1.0.8", + "@acdh-oeaw/eslint-config-nuxt": "^1.0.14", + "@acdh-oeaw/eslint-config-playwright": "^1.0.8", + "@acdh-oeaw/eslint-config-vue": "^1.0.13", "@acdh-oeaw/prettier-config": "^2.0.0", "@acdh-oeaw/stylelint-config": "^2.0.1", "@acdh-oeaw/tailwindcss-preset": "^0.0.22", - "@nuxt/devtools": "^1.0.8", - "@playwright/test": "^1.42.1", - "@tanstack/eslint-plugin-query": "^5.27.7", + "@acdh-oeaw/tsconfig": "^1.1.0", + "@nuxt/devtools": "^1.3.6", + "@playwright/test": "^1.44.1", + "@tanstack/eslint-plugin-query": "^5.43.1", "@types/geojson": "^7946.0.14", - "@types/node": "^20.11.28", - "axe-core": "^4.8.4", + "@types/node": "^20.14.7", + "axe-core": "^4.9.1", "axe-playwright": "^2.0.1", "ci-info": "^4.0.0", - "decap-server": "^3.0.2", + "decap-server": "^3.0.4", + "dotenv": "^16.4.5", + "dotenv-cli": "^7.4.2", + "dotenv-expand": "^11.0.6", "eslint": "^8.57.0", - "eslint-plugin-tailwindcss": "^3.15.1", + "eslint-plugin-tailwindcss": "^3.17.4", "is-ci": "^3.0.1", - "lint-staged": "^15.2.2", - "postcss": "^8.4.36", - "prettier": "^3.2.5", + "lint-staged": "^15.2.7", + "npm-run-all2": "^6.2.0", + "postcss": "^8.4.38", + "prettier": "^3.3.2", "schema-dts": "^1.1.2", - "simple-git-hooks": "^2.11.0", - "stylelint": "^16.2.1", - "tailwindcss": "^3.4.1", - "typescript": "^5.4.2", - "vite": "^5.1.6", - "vue-tsc": "^2.0.6" + "simple-git-hooks": "^2.11.1", + "stylelint": "^16.6.1", + "tailwindcss": "^3.4.4", + "tsx": "^4.15.7", + "typescript": "^5.5.2", + "vite": "^5.3.1", + "vue-tsc": "^2.0.21" }, "browserslist": { "development": [ @@ -139,6 +139,7 @@ "@acdh-oeaw/eslint-config/strict", "@acdh-oeaw/eslint-config-vue", "@acdh-oeaw/eslint-config-nuxt", + "@acdh-oeaw/eslint-config-playwright", "plugin:@tanstack/eslint-plugin-query/recommended", "plugin:eslint-plugin-tailwindcss/recommended" ], diff --git a/pages/data/index.vue b/pages/data/index.vue index 90a4deb1..d3e6e685 100644 --- a/pages/data/index.vue +++ b/pages/data/index.vue @@ -2,7 +2,7 @@ definePageMeta({ validate() { const env = useRuntimeConfig(); - return env.public.NUXT_PUBLIC_DATABASE !== "disabled"; + return env.public.database !== "disabled"; }, }); @@ -21,7 +21,7 @@ const env = useRuntimeConfig(); {{ t("DataPage.title") }} -