From c70afc34b0e3c350755eed4366fb912944888637 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Mon, 24 Jun 2024 21:59:51 +0200 Subject: [PATCH 1/4] pkg: Drop extension in timeformat imports We are about to rename it to *.ts. --- pkg/lib/cockpit-components-shutdown.jsx | 2 +- pkg/lib/serverTime.js | 2 +- pkg/packagekit/history.jsx | 2 +- pkg/packagekit/updates.jsx | 2 +- pkg/storaged/crypto/encryption.jsx | 2 +- pkg/systemd/overview-cards/systemInformationCard.jsx | 2 +- pkg/systemd/timer-dialog.jsx | 2 +- pkg/users/account-details.js | 2 +- pkg/users/account-logs-panel.jsx | 2 +- pkg/users/accounts-list.js | 2 +- pkg/users/expiration-dialogs.js | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/lib/cockpit-components-shutdown.jsx b/pkg/lib/cockpit-components-shutdown.jsx index 53c70d8c841..4e16f85a23d 100644 --- a/pkg/lib/cockpit-components-shutdown.jsx +++ b/pkg/lib/cockpit-components-shutdown.jsx @@ -31,7 +31,7 @@ import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicke import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js"; import { ServerTime } from 'serverTime.js'; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import { DialogsContext } from "dialogs.jsx"; import { FormHelper } from "cockpit-components-form-helper"; diff --git a/pkg/lib/serverTime.js b/pkg/lib/serverTime.js index 3fb3e743ce1..bd387139372 100644 --- a/pkg/lib/serverTime.js +++ b/pkg/lib/serverTime.js @@ -32,7 +32,7 @@ import { show_modal_dialog } from "cockpit-components-dialog.jsx"; import { useObject, useEvent } from "hooks.js"; import * as service from "service.js"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import * as python from "python.js"; import get_timesync_backend_py from "./get-timesync-backend.py"; diff --git a/pkg/packagekit/history.jsx b/pkg/packagekit/history.jsx index 6a5e681065e..08f84f2e723 100644 --- a/pkg/packagekit/history.jsx +++ b/pkg/packagekit/history.jsx @@ -23,7 +23,7 @@ import PropTypes from "prop-types"; import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js"; import { BundleIcon } from "@patternfly/react-icons"; import { ListingTable } from "cockpit-components-table.jsx"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import cockpit from "cockpit"; diff --git a/pkg/packagekit/updates.jsx b/pkg/packagekit/updates.jsx index 5948798bded..e11d440018c 100644 --- a/pkg/packagekit/updates.jsx +++ b/pkg/packagekit/updates.jsx @@ -71,7 +71,7 @@ import { WithDialogs } from "dialogs.jsx"; import { superuser } from 'superuser'; import * as PK from "packagekit.js"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import * as python from "python.js"; import callTracerScript from './callTracer.py'; diff --git a/pkg/storaged/crypto/encryption.jsx b/pkg/storaged/crypto/encryption.jsx index 4660d602b4b..fb881952481 100644 --- a/pkg/storaged/crypto/encryption.jsx +++ b/pkg/storaged/crypto/encryption.jsx @@ -25,7 +25,7 @@ import { DescriptionList } from "@patternfly/react-core/dist/esm/components/Desc import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js"; import { useObject, useEvent } from "hooks"; import * as python from "python.js"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import { dialog_open, TextInput, PassInput } from "../dialog.jsx"; import { block_name, encode_filename, decode_filename, parse_options, unparse_options, extract_option, edit_crypto_config } from "../utils.js"; diff --git a/pkg/systemd/overview-cards/systemInformationCard.jsx b/pkg/systemd/overview-cards/systemInformationCard.jsx index a949d90b318..bd83df70268 100644 --- a/pkg/systemd/overview-cards/systemInformationCard.jsx +++ b/pkg/systemd/overview-cards/systemInformationCard.jsx @@ -22,7 +22,7 @@ import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/di import cockpit from "cockpit"; import * as machine_info from "machine-info.js"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import "./systemInformationCard.scss"; diff --git a/pkg/systemd/timer-dialog.jsx b/pkg/systemd/timer-dialog.jsx index 71b21f2abab..9d1f04ca9d7 100644 --- a/pkg/systemd/timer-dialog.jsx +++ b/pkg/systemd/timer-dialog.jsx @@ -37,7 +37,7 @@ import { useDialogs } from "dialogs.jsx"; import { updateTime } from './services.jsx'; import { create_timer } from './timer-dialog-helpers.js'; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import "./timers.scss"; diff --git a/pkg/users/account-details.js b/pkg/users/account-details.js index 172ecd21e93..3a36b62cfd8 100644 --- a/pkg/users/account-details.js +++ b/pkg/users/account-details.js @@ -38,7 +38,7 @@ import { ExclamationCircleIcon, HelpIcon, UndoIcon } from '@patternfly/react-ico import cockpit from 'cockpit'; import { superuser } from "superuser"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import { apply_modal_dialog } from "cockpit-components-dialog.jsx"; import { show_unexpected_error } from "./dialog-utils.js"; diff --git a/pkg/users/account-logs-panel.jsx b/pkg/users/account-logs-panel.jsx index 9d4da82f44c..e8ad639562b 100644 --- a/pkg/users/account-logs-panel.jsx +++ b/pkg/users/account-logs-panel.jsx @@ -23,7 +23,7 @@ import React, { useState } from 'react'; import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js"; import { ListingTable } from 'cockpit-components-table.jsx'; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import { useInit } from "hooks"; const _ = cockpit.gettext; diff --git a/pkg/users/accounts-list.js b/pkg/users/accounts-list.js index 32766c12616..5cb9529b029 100644 --- a/pkg/users/accounts-list.js +++ b/pkg/users/accounts-list.js @@ -35,7 +35,7 @@ import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchIn import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js"; import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import { EmptyStatePanel } from 'cockpit-components-empty-state.jsx'; import { ListingTable } from 'cockpit-components-table.jsx'; import { SearchIcon } from '@patternfly/react-icons'; diff --git a/pkg/users/expiration-dialogs.js b/pkg/users/expiration-dialogs.js index 2e117b1cc1a..8e1b87ba9a6 100644 --- a/pkg/users/expiration-dialogs.js +++ b/pkg/users/expiration-dialogs.js @@ -27,7 +27,7 @@ import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicke import { has_errors } from "./dialog-utils.js"; import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx"; -import * as timeformat from "timeformat.js"; +import * as timeformat from "timeformat"; import { FormHelper } from "cockpit-components-form-helper"; const _ = cockpit.gettext; From 7f8829b3f9edd48da54149b6480d2b03826236f5 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Mon, 24 Jun 2024 22:35:30 +0200 Subject: [PATCH 2/4] lib: Type-annotate timeformat Turn the optional `addSuffix` parameter of `distanceToNow()` into a proper boolean, to match `formatDistanceToNow()`'s signature. We have to apply an "any" crowbar for the "magic" locale import. But that's not exposed in the API, just an internal implementation detail. --- pkg/lib/{timeformat.js => timeformat.ts} | 43 ++++++++++++++---------- 1 file changed, 26 insertions(+), 17 deletions(-) rename pkg/lib/{timeformat.js => timeformat.ts} (51%) diff --git a/pkg/lib/timeformat.js b/pkg/lib/timeformat.ts similarity index 51% rename from pkg/lib/timeformat.js rename to pkg/lib/timeformat.ts index 4995e3117ad..6733abfcaf6 100644 --- a/pkg/lib/timeformat.js +++ b/pkg/lib/timeformat.ts @@ -8,47 +8,56 @@ import { parse, formatDistanceToNow } from 'date-fns'; import * as locales from 'date-fns/locale'; // this needs to be dynamic, as some pages don't initialize cockpit.language right away -export const dateFormatLang = () => cockpit.language.replace('_', '-'); +export const dateFormatLang = (): string => cockpit.language.replace('_', '-'); -const dateFormatLangDateFns = () => { +const dateFormatLangDateFns = (): string => { if (cockpit.language == "en") return "enUS"; else return cockpit.language.replace('_', ''); }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const dateFnsLocale = (): any => (locales as any)[dateFormatLangDateFns()]; + +type Time = Date | number; + // general Intl.DateTimeFormat formatter object -export const formatter = options => new Intl.DateTimeFormat(dateFormatLang(), options); +export const formatter = (options?: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat(dateFormatLang(), options); // common formatters; try to use these as much as possible, for UI consistency // 07:41 AM -export const time = t => formatter({ timeStyle: "short" }).format(t); +export const time = (t: Time): string => formatter({ timeStyle: "short" }).format(t); // 7:41:26 AM -export const timeSeconds = t => formatter({ timeStyle: "medium" }).format(t); +export const timeSeconds = (t: Time): string => formatter({ timeStyle: "medium" }).format(t); // June 30, 2021 -export const date = t => formatter({ dateStyle: "long" }).format(t); +export const date = (t: Time): string => formatter({ dateStyle: "long" }).format(t); // 06/30/2021 -export const dateShort = t => formatter().format(t); +export const dateShort = (t: Time): string => formatter().format(t); // Jun 30, 2021, 7:41 AM -export const dateTime = t => formatter({ dateStyle: "medium", timeStyle: "short" }).format(t); +export const dateTime = (t: Time): string => formatter({ dateStyle: "medium", timeStyle: "short" }).format(t); // Jun 30, 2021, 7:41:23 AM -export const dateTimeSeconds = t => formatter({ dateStyle: "medium", timeStyle: "medium" }).format(t); +export const dateTimeSeconds = (t: Time): string => formatter({ dateStyle: "medium", timeStyle: "medium" }).format(t); // Jun 30, 7:41 AM -export const dateTimeNoYear = t => formatter({ month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(t); +export const dateTimeNoYear = (t: Time): string => formatter({ month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(t); // Wednesday, June 30, 2021 -export const weekdayDate = t => formatter({ dateStyle: "full" }).format(t); +export const weekdayDate = (t: Time): string => formatter({ dateStyle: "full" }).format(t); // The following options are helpful for placeholders // yyyy/mm/dd -export const dateShortFormat = () => locales[dateFormatLangDateFns()].formatLong.date({ width: 'short' }); +export const dateShortFormat = (): string => dateFnsLocale().formatLong.date({ width: 'short' }); // about 1 hour [ago] -export const distanceToNow = (t, addSuffix) => formatDistanceToNow(t, { locale: locales[dateFormatLangDateFns()], addSuffix }); +export const distanceToNow = (t: Time, addSuffix?: boolean): string => formatDistanceToNow(t, { + locale: dateFnsLocale(), + addSuffix: addSuffix ?? false +}); // Parse a string localized date like 30.06.21 to a Date Object -export function parseShortDate(dateStr) { +export function parseShortDate(dateStr: string): Date { const parsed = parse(dateStr, dateShortFormat(), new Date()); // Strip time which may cause bugs in calendar - const timePortion = parsed.getTime() % (3600 * 1000 * 24); - return new Date(parsed - timePortion); + const time = parsed.getTime(); + const timePortion = time % (3600 * 1000 * 24); + return new Date(time - timePortion); } /*** @@ -61,6 +70,6 @@ export function parseShortDate(dateStr) { const first_dow_sun = ['en', 'ja', 'ko', 'pt', 'pt_BR', 'sv', 'zh_CN', 'zh_TW']; -export function firstDayOfWeek() { +export function firstDayOfWeek(): number { return first_dow_sun.indexOf(cockpit.language) >= 0 ? 0 : 1; } From d354edd7d382c80527d92c4d399c11f350a0cd9e Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Mon, 24 Jun 2024 22:59:28 +0200 Subject: [PATCH 3/4] base1: Add unit test for timeformat library Pay special attention to the relative date formatting, as we want to replace it (#20653). We don't need to replicate the exact behaviour, but should have some coverage. This finds a bug in parseShortDate(). Add a comment and skip the test. --- files.js | 1 + pkg/base1/test-timeformat.ts | 150 +++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 pkg/base1/test-timeformat.ts diff --git a/files.js b/files.js index 919c064b1a8..01aa274eabc 100644 --- a/files.js +++ b/files.js @@ -70,6 +70,7 @@ const info = { "base1/test-spawn-proc.js", "base1/test-spawn.js", "base1/test-stream.js", + "base1/test-timeformat.ts", "base1/test-types.ts", "base1/test-user.js", "base1/test-websocket.js", diff --git a/pkg/base1/test-timeformat.ts b/pkg/base1/test-timeformat.ts new file mode 100644 index 00000000000..836c78b5721 --- /dev/null +++ b/pkg/base1/test-timeformat.ts @@ -0,0 +1,150 @@ +import QUnit from 'qunit-tests'; + +import cockpit from "cockpit"; +import * as timeformat from "timeformat"; + +const d1 = new Date("2024-01-02 03:04:05"); + +QUnit.test("absolute formatters, English", assert => { + cockpit.language = "en"; + assert.equal(timeformat.time(d1), "3:04 AM"); + assert.equal(timeformat.timeSeconds(d1), "3:04:05 AM"); + assert.equal(timeformat.date(d1), "January 2, 2024"); + assert.equal(timeformat.dateShort(d1), "1/2/2024"); + assert.equal(timeformat.dateTime(d1), "Jan 2, 2024, 3:04 AM"); + assert.equal(timeformat.dateTimeSeconds(d1), "Jan 2, 2024, 3:04:05 AM"); + assert.equal(timeformat.dateTimeNoYear(d1), "Jan 02, 03:04 AM"); + assert.equal(timeformat.weekdayDate(d1), "Tuesday, January 2, 2024"); + + assert.equal(timeformat.dateShortFormat(), "MM/dd/yyyy"); + + // all of these work with numbers as time argument + assert.equal(timeformat.dateTimeSeconds(d1.valueOf()), "Jan 2, 2024, 3:04:05 AM"); +}); + +QUnit.test("absolute formatters, German", assert => { + cockpit.language = "de"; + assert.equal(timeformat.time(d1), "03:04"); + assert.equal(timeformat.timeSeconds(d1), "03:04:05"); + assert.equal(timeformat.date(d1), "2. Januar 2024"); + assert.equal(timeformat.dateShort(d1), "2.1.2024"); + assert.equal(timeformat.dateTime(d1), "02.01.2024, 03:04"); + assert.equal(timeformat.dateTimeSeconds(d1), "02.01.2024, 03:04:05"); + assert.equal(timeformat.dateTimeNoYear(d1), "02. Jan., 03:04"); + assert.equal(timeformat.weekdayDate(d1), "Dienstag, 2. Januar 2024"); + + assert.equal(timeformat.dateShortFormat(), "dd.MM.y"); + + // all of these work with numbers as time argument + assert.equal(timeformat.dateTimeSeconds(d1.valueOf()), "02.01.2024, 03:04:05"); +}); + +QUnit.test("absolute formatters, per-country locale", assert => { + cockpit.language = "en_GB"; + assert.equal(timeformat.timeSeconds(d1), "03:04:05"); + assert.equal(timeformat.date(d1), "2 January 2024"); + assert.equal(timeformat.dateShort(d1), "02/01/2024"); + assert.equal(timeformat.dateShortFormat(), "dd/MM/yyyy"); + + cockpit.language = "pt"; + assert.equal(timeformat.date(d1), "2 de janeiro de 2024"); + + cockpit.language = "pt_BR"; + assert.equal(timeformat.date(d1), "2 de janeiro de 2024"); + + cockpit.language = "zh_CN"; + assert.equal(timeformat.weekdayDate(d1), "2024年1月2日星期二"); + + cockpit.language = "zh_TW"; + assert.equal(timeformat.weekdayDate(d1), "2024年1月2日 星期二"); +}); + +const SEC = 1000; +const MIN = 60 * SEC; +const HOUR = 60 * MIN; +const DAY = 24 * HOUR; + +QUnit.test("relative formatter, English", assert => { + const now = Date.now(); + const seconds = now - 4.5 * SEC; + const minutes = now - 4 * MIN - 5 * SEC; + const halfhour = now - 32 * MIN; + const hours = now - 3 * HOUR - 57 * MIN; + const days4short = now - 4 * DAY + 2 * HOUR; + const days4long = now - 4 * DAY - 2 * HOUR; + const week3short = now - 20 * DAY + 2 * HOUR; + const week3long = now - 21 * DAY - 2 * HOUR; + const month2short = now - 2 * 30 * DAY + 5 * DAY; + const month2long = now - 2 * 30 * DAY - 5 * DAY; + + cockpit.language = "en"; + assert.equal(timeformat.distanceToNow(seconds), "less than a minute"); + assert.equal(timeformat.distanceToNow(seconds, false), "less than a minute"); + assert.equal(timeformat.distanceToNow(seconds, true), "less than a minute ago"); + + assert.equal(timeformat.distanceToNow(minutes), "4 minutes"); + assert.equal(timeformat.distanceToNow(minutes, true), "4 minutes ago"); + + assert.equal(timeformat.distanceToNow(halfhour), "32 minutes"); + assert.equal(timeformat.distanceToNow(halfhour, true), "32 minutes ago"); + + assert.equal(timeformat.distanceToNow(hours), "about 4 hours"); + assert.equal(timeformat.distanceToNow(hours, true), "about 4 hours ago"); + + assert.equal(timeformat.distanceToNow(days4short), "4 days"); + assert.equal(timeformat.distanceToNow(days4long), "4 days"); + assert.equal(timeformat.distanceToNow(days4short, true), "4 days ago"); + + assert.equal(timeformat.distanceToNow(week3short), "20 days"); + assert.equal(timeformat.distanceToNow(week3long), "21 days"); + assert.equal(timeformat.distanceToNow(week3long, true), "21 days ago"); + + assert.equal(timeformat.distanceToNow(month2short), "about 2 months"); + assert.equal(timeformat.distanceToNow(month2long), "2 months"); +}); + +QUnit.test("relative formatter, German", assert => { + const now = Date.now(); + const seconds = now - 4.5 * SEC; + + // no need to be as thorough as with English, just spot check that it's translated + cockpit.language = "de"; + assert.equal(timeformat.distanceToNow(seconds), "weniger als 1 Minute"); + assert.equal(timeformat.distanceToNow(seconds, true), "vor weniger als 1 Minute"); + + assert.equal(timeformat.distanceToNow(now - 4 * DAY), "4 Tage"); + assert.equal(timeformat.distanceToNow(now - 21 * DAY), "21 Tage"); + assert.equal(timeformat.distanceToNow(now - 62 * DAY), "2 Monate"); +}); + +QUnit.test("firstDayOfWeek", assert => { + cockpit.language = "en"; + assert.equal(timeformat.firstDayOfWeek(), 0); + cockpit.language = "de"; + assert.equal(timeformat.firstDayOfWeek(), 1); +}); + +// FIXME: This test is currently time zone dependent; parseShortDate() always +// interprets its argument as midnight UTC instead of local time; so it's even +// off by a day for any TZ east of UTC. +QUnit.skip("parsing", assert => { + cockpit.language = "en"; + const en = timeformat.parseShortDate("1/20/2024"); + assert.equal(en.getDate(), 20); + assert.equal(en.getMonth(), 0); // yes, starting from 0 + assert.equal(en.getFullYear(), "2024"); + + cockpit.language = "en_GB"; + const engb = timeformat.parseShortDate("20/01/2024"); + assert.equal(engb.getDate(), 20); + assert.equal(engb.getMonth(), 0); // yes, starting from 0 + assert.equal(engb.getFullYear(), "2024"); + + cockpit.language = "de"; + const de = timeformat.parseShortDate("20.01.2024"); + assert.equal(de.getDate(), 20); + assert.equal(de.getMonth(), 0); // yes, starting from 0 + assert.equal(de.getFullYear(), "2024"); +}); + +QUnit.start(); From d8cf4f41284200872d81f13e2d68583defea9eaa Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Tue, 25 Jun 2024 08:12:57 +0200 Subject: [PATCH 4/4] lib: Fix timeformat.parseShortDate() Do the "strip off time and set to midnight" step already for the reference Date input of parse(). Substracting it afterwards previously caused the returned date to be off by one day for time zones other than UTC. Add `test_timeformat_timezones()` pytest to cover various time zones. --- pkg/base1/test-timeformat.ts | 5 +---- pkg/lib/timeformat.ts | 10 ++++------ test/pytest/test_browser.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pkg/base1/test-timeformat.ts b/pkg/base1/test-timeformat.ts index 836c78b5721..ba926142271 100644 --- a/pkg/base1/test-timeformat.ts +++ b/pkg/base1/test-timeformat.ts @@ -124,10 +124,7 @@ QUnit.test("firstDayOfWeek", assert => { assert.equal(timeformat.firstDayOfWeek(), 1); }); -// FIXME: This test is currently time zone dependent; parseShortDate() always -// interprets its argument as midnight UTC instead of local time; so it's even -// off by a day for any TZ east of UTC. -QUnit.skip("parsing", assert => { +QUnit.test("parsing", assert => { cockpit.language = "en"; const en = timeformat.parseShortDate("1/20/2024"); assert.equal(en.getDate(), 20); diff --git a/pkg/lib/timeformat.ts b/pkg/lib/timeformat.ts index 6733abfcaf6..b16dd54d8c1 100644 --- a/pkg/lib/timeformat.ts +++ b/pkg/lib/timeformat.ts @@ -52,12 +52,10 @@ export const distanceToNow = (t: Time, addSuffix?: boolean): string => formatDis // Parse a string localized date like 30.06.21 to a Date Object export function parseShortDate(dateStr: string): Date { - const parsed = parse(dateStr, dateShortFormat(), new Date()); - - // Strip time which may cause bugs in calendar - const time = parsed.getTime(); - const timePortion = time % (3600 * 1000 * 24); - return new Date(time - timePortion); + // strip off time (i.e. set to midnight), to avoid confusing calendar widgets + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + return parse(dateStr, dateShortFormat(), today); } /*** diff --git a/test/pytest/test_browser.py b/test/pytest/test_browser.py index d02daa5b03e..38dc0e904d4 100644 --- a/test/pytest/test_browser.py +++ b/test/pytest/test_browser.py @@ -46,3 +46,22 @@ def test_browser(html): subprocess.run(['test/common/tap-cdp', f'{BUILDDIR}/test-server', sys.executable, '-m', *coverage, 'cockpit.bridge', '--debug', f'./qunit/{html}'], check=True, stderr=subprocess.STDOUT) + + +# run test-timeformat.ts in different time zones: west/UTC/east +@pytest.mark.parametrize('tz', ['America/Toronto', 'Europe/London', 'UTC', 'Europe/Berlin', 'Australia/Sydney']) +def test_timeformat_timezones(tz): + if not os.path.exists(f'{BUILDDIR}/test-server'): + pytest.skip('no test-server') + # this doesn't get built in rpm/deb package build environments, similar to test_browser() + built_test = './qunit/base1/test-timeformat.html' + if not os.path.exists(built_test): + pytest.skip(f'{built_test} not found') + + env = os.environ.copy() + env['TZ'] = tz + + # Merge 2>&1 so that pytest displays an interleaved log + subprocess.run(['test/common/tap-cdp', f'{BUILDDIR}/test-server', + sys.executable, '-m', 'cockpit.bridge', '--debug', + built_test], check=True, stderr=subprocess.STDOUT, env=env)