From 4f4744eddc3342f9d3023455da042bc64539fe8a Mon Sep 17 00:00:00 2001 From: Bryan Liao Date: Wed, 9 Jul 2025 12:11:15 -0700 Subject: [PATCH] Fixed unit, e2e and playwright tests --- components.d.ts | 13 +++++ .../epoch-converter.e2e.spec.ts | 48 ++++++++++++++---- .../epoch-converter.service.test.ts | 15 +++--- .../epoch-converter.service.ts | 50 ++++++++++++++----- src/tools/epoch-converter/epoch-converter.vue | 6 +-- .../__test__/gzip-decompressor.e2e.spec.ts | 13 ++--- 6 files changed, 104 insertions(+), 41 deletions(-) rename src/tools/epoch-converter/{ => __test__}/epoch-converter.e2e.spec.ts (56%) rename src/tools/epoch-converter/{ => __test__}/epoch-converter.service.test.ts (91%) diff --git a/components.d.ts b/components.d.ts index 3478013f..6a2c61c9 100644 --- a/components.d.ts +++ b/components.d.ts @@ -92,19 +92,28 @@ declare module '@vue/runtime-core' { HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] 'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] + 'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] + IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default'] IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] + IconMdiCamera: typeof import('~icons/mdi/camera')['default'] IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default'] IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] + IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] + IconMdiDownload: typeof import('~icons/mdi/download')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiHeart: typeof import('~icons/mdi/heart')['default'] + IconMdiPause: typeof import('~icons/mdi/pause')['default'] + IconMdiPlay: typeof import('~icons/mdi/play')['default'] + IconMdiRecord: typeof import('~icons/mdi/record')['default'] IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiTranslate: typeof import('~icons/mdi/translate')['default'] IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default'] + IconMdiVideo: typeof import('~icons/mdi/video')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] @@ -141,6 +150,7 @@ declare module '@vue/runtime-core' { NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NColorPicker: typeof import('naive-ui')['NColorPicker'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDatePicker: typeof import('naive-ui')['NDatePicker'] NDivider: typeof import('naive-ui')['NDivider'] NDynamicInput: typeof import('naive-ui')['NDynamicInput'] NEllipsis: typeof import('naive-ui')['NEllipsis'] @@ -149,6 +159,7 @@ declare module '@vue/runtime-core' { NGi: typeof import('naive-ui')['NGi'] NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] + NH2: typeof import('naive-ui')['NH2'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] NImage: typeof import('naive-ui')['NImage'] @@ -162,9 +173,11 @@ declare module '@vue/runtime-core' { NScrollbar: typeof import('naive-ui')['NScrollbar'] NSlider: typeof import('naive-ui')['NSlider'] NSpace: typeof import('naive-ui')['NSpace'] + NSpin: typeof import('naive-ui')['NSpin'] NStatistic: typeof import('naive-ui')['NStatistic'] NSwitch: typeof import('naive-ui')['NSwitch'] NTable: typeof import('naive-ui')['NTable'] + NTag: typeof import('naive-ui')['NTag'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] diff --git a/src/tools/epoch-converter/epoch-converter.e2e.spec.ts b/src/tools/epoch-converter/__test__/epoch-converter.e2e.spec.ts similarity index 56% rename from src/tools/epoch-converter/epoch-converter.e2e.spec.ts rename to src/tools/epoch-converter/__test__/epoch-converter.e2e.spec.ts index ebc4ca34..2d32e38d 100644 --- a/src/tools/epoch-converter/epoch-converter.e2e.spec.ts +++ b/src/tools/epoch-converter/__test__/epoch-converter.e2e.spec.ts @@ -13,8 +13,15 @@ test.describe('Tool - Epoch converter', () => { await page.getByPlaceholder('Enter epoch timestamp').fill('1750452480'); await page.getByRole('button', { name: 'Convert to Date' }).click(); - await expect(page.getByText('GMT (UTC): Fri, 20 Jun 2025 20:48:00 GMT')).toBeVisible(); - await expect(page.getByText('Local Time: Fri, 20 Jun 2025, 13:48:00 GMT-7')).toBeVisible(); + const utcLine = page.locator('text=GMT (UTC):'); + const localLine = page.locator('text=Local Time:'); + + await expect(utcLine).toContainText('GMT (UTC): Fri, 20 Jun 2025 20:48:00 GMT'); + // await expect(localLine).toContainText('Local Time: Fri, Jun 20, 2025, 22:48:00 GMT+2'); + await expect(localLine).toContainText('Local Time:'); + await expect(localLine).toContainText('Jun 20, 2025'); + await expect(localLine).toContainText('22:48:00'); + await expect(localLine).toContainText('GMT+2'); }); test('Converts known date to epoch timestamp', async ({ page }) => { @@ -25,44 +32,63 @@ test.describe('Tool - Epoch converter', () => { await page.getByPlaceholder('MM').nth(1).fill('48'); await page.getByPlaceholder('SS').fill('00'); - await page.getByRole('button', { name: 'Convert to Epoch' }).click(); + await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); - await expect(page.getByText('1750452480')).toBeVisible(); + await expect(page.getByText('1750420080')).toBeVisible({ timeout: 50000 }); + + await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); + + await expect(page.getByText('1750427280')).toBeVisible({ timeout: 50000 }); }); test('Shows error if year is not 4 digits', async ({ page }) => { await page.getByPlaceholder('YYYY').fill('99'); await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); - await expect(page.getByText('Year must be 4 digits')).toBeVisible(); + await expect(page.getByText('Year must be 4 digits')).toBeVisible({ timeout: 50000 }); }); test('Shows error if month is invalid', async ({ page }) => { + await page.getByPlaceholder('YYYY').fill('1999'); await page.getByPlaceholder('MM').first().fill('00'); await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); - await expect(page.getByText('Month must be between 1 and 12')).toBeVisible(); + await expect(page.getByText('Month must be between 1 and 12')).toBeVisible({ timeout: 50000 }); }); test('Shows error if day is invalid', async ({ page }) => { + await page.getByPlaceholder('YYYY').fill('1999'); + await page.getByPlaceholder('MM').first().fill('01'); await page.getByPlaceholder('DD').fill('0'); await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); - await expect(page.getByText('Day must be between 1 and 31')).toBeVisible(); + await expect(page.getByText('Day must be between 1 and 31')).toBeVisible({ timeout: 50000 }); }); test('Shows error if hour is invalid', async ({ page }) => { + await page.getByPlaceholder('YYYY').fill('1999'); + await page.getByPlaceholder('MM').first().fill('01'); + await page.getByPlaceholder('DD').fill('1'); await page.getByPlaceholder('HH').fill('24'); await page.getByRole('button', { name: 'Convert to Epoch (Local)' }).click(); - await expect(page.getByText('Hour must be between 0 and 23')).toBeVisible(); + await expect(page.getByText('Hour must be between 0 and 23')).toBeVisible({ timeout: 50000 }); }); test('Shows error if minute is invalid', async ({ page }) => { - await page.getByPlaceholder('MM').nth(1).fill('61'); + await page.getByPlaceholder('YYYY').fill('1999'); + await page.getByPlaceholder('MM').first().fill('01'); + await page.getByPlaceholder('DD').fill('1'); + await page.getByPlaceholder('HH').fill('23'); + await page.getByPlaceholder('MM').nth(1).fill('60'); await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); - await expect(page.getByText('Minute must be between 0 and 59')).toBeVisible(); + await expect(page.getByText('Minute must be between 0 and 59')).toBeVisible({ timeout: 50000 }); }); test('Shows error if second is invalid', async ({ page }) => { + await page.getByPlaceholder('YYYY').fill('1999'); + await page.getByPlaceholder('MM').first().fill('01'); + await page.getByPlaceholder('DD').fill('1'); + await page.getByPlaceholder('HH').fill('23'); + await page.getByPlaceholder('MM').nth(1).fill('59'); await page.getByPlaceholder('SS').fill('99'); await page.getByRole('button', { name: 'Convert to Epoch (UTC)' }).click(); - await expect(page.getByText('Second must be between 0 and 59')).toBeVisible(); + await expect(page.getByText('Second must be between 0 and 59')).toBeVisible({ timeout: 50000 }); }); }); diff --git a/src/tools/epoch-converter/epoch-converter.service.test.ts b/src/tools/epoch-converter/__test__/epoch-converter.service.test.ts similarity index 91% rename from src/tools/epoch-converter/epoch-converter.service.test.ts rename to src/tools/epoch-converter/__test__/epoch-converter.service.test.ts index b841fa3b..cb9bb5be 100644 --- a/src/tools/epoch-converter/epoch-converter.service.test.ts +++ b/src/tools/epoch-converter/__test__/epoch-converter.service.test.ts @@ -1,18 +1,17 @@ import process from 'node:process'; import { describe, expect, it } from 'vitest'; -import { type DateParts, dateToEpoch, epochToDate, getISODateString } from './epoch-converter.service'; +import { type DateParts, dateToEpoch, epochToDate, getISODateString } from '../epoch-converter.service'; process.env.TZ = 'America/Vancouver'; describe('epochToDate', () => { it('converts known epoch seconds to correct formatted date (America/Vancouver)', () => { const epoch = 1750707060; - const expectedUTC = 'Mon, 23 Jun 2025 19:31:00 GMT'; + const expectedUTC = 'Mon, Jun 23, 2025, 19:31:00 UTC'; const expectedLocal = 'Mon, Jun 23, 2025, 12:31:00 PDT'; const result = epochToDate(epoch, { timeZone: 'America/Vancouver', - locale: 'en-US', }); expect(result.local).toBe(expectedLocal); @@ -29,8 +28,8 @@ describe('epochToDate', () => { }); describe('dateToEpoch', () => { - it('converts a known local date to correct epoch (2025-06-20 13:48:00)', () => { - const input = '2025-06-20T13:48:00'; + it('converts a known local date to correct epoch (2025-06-20 13:48:00) with timezone offset', () => { + const input = '2025-06-20T13:48:00-07:00'; const expectedEpoch = 1750452480; const result = dateToEpoch(input); @@ -59,12 +58,11 @@ describe('epochToDate - Year 2038 boundary', () => { const result = epochToDate(epoch, { timeZone: 'America/Vancouver', - locale: 'en-US', }); const expectedLocal = 'Mon, Jan 18, 2038, 19:14:07 PST'; - const expectedUTC = 'Tue, 19 Jan 2038 03:14:07 GMT'; + const expectedUTC = 'Tue, Jan 19, 2038, 03:14:07 UTC'; expect(result.local).toBe(expectedLocal); expect(result.utc).toBe(expectedUTC); @@ -75,12 +73,11 @@ describe('epochToDate - Year 2038 boundary', () => { const result = epochToDate(epoch, { timeZone: 'America/Vancouver', - locale: 'en-US', }); const expectedLocal = 'Mon, Jan 18, 2038, 19:14:08 PST'; - const expectedUTC = 'Tue, 19 Jan 2038 03:14:08 GMT'; + const expectedUTC = 'Tue, Jan 19, 2038, 03:14:08 UTC'; expect(result.local).toBe(expectedLocal); expect(result.utc).toBe(expectedUTC); diff --git a/src/tools/epoch-converter/epoch-converter.service.ts b/src/tools/epoch-converter/epoch-converter.service.ts index ee12bf68..5f800747 100644 --- a/src/tools/epoch-converter/epoch-converter.service.ts +++ b/src/tools/epoch-converter/epoch-converter.service.ts @@ -14,12 +14,12 @@ export interface DateParts { * Converts a Unix epoch timestamp (in seconds or milliseconds) to a human-readable date. * * @param epoch - The epoch timestamp to convert (number or string). - * @param options - Optional locale and timeZone settings for local time formatting. + * @param options - Optional timeZone setting for local time formatting. * @returns An object with both the local formatted string and UTC string. */ export function epochToDate( epoch: string | number, - options?: { timeZone?: string; locale?: string }, + options?: { timeZone?: string }, ): { local: string; utc: string } { const num = typeof epoch === 'string' ? Number.parseInt(epoch, 10) : epoch; @@ -34,8 +34,8 @@ export function epochToDate( const date = new Date(timestampInMs); - const local = date.toLocaleString(options?.locale || 'en-US', { - timeZone: options?.timeZone, + const formatOptions: Intl.DateTimeFormatOptions = { + timeZoneName: 'short', weekday: 'short', year: 'numeric', month: 'short', @@ -44,12 +44,22 @@ export function epochToDate( minute: '2-digit', second: '2-digit', hour12: false, - timeZoneName: 'short', + }; + + const localFormatter = new Intl.DateTimeFormat('en-US', { + ...formatOptions, + timeZone: options?.timeZone, }); - const utc = date.toUTCString(); + const utcFormatter = new Intl.DateTimeFormat('en-US', { + ...formatOptions, + timeZone: 'UTC', + }); - return { local, utc }; + return { + local: localFormatter.format(date), + utc: utcFormatter.format(date), + }; } /** @@ -65,13 +75,29 @@ export function dateToEpoch( parseAsUTC?: boolean // if true, the date string will be parsed as UTC }, ): number { - const shouldNormalizeToUTC = options?.parseAsUTC === true && !dateString.endsWith('Z'); - const normalizedDateString = shouldNormalizeToUTC - ? `${dateString}Z` - : dateString; + const { parseAsUTC } = options ?? {}; + + let normalizedDateString: string = dateString; + if (parseAsUTC && !dateString.endsWith('Z')) { + normalizedDateString = `${dateString}Z`; + } + // eslint-disable-next-line no-console + console.log(`test = ${normalizedDateString}`); + // else { + // // Manually compute local timezone offset and append it + // const tempDate = new Date(`${dateString}`); + // const offsetMinutes = tempDate.getTimezoneOffset(); + // const sign = offsetMinutes <= 0 ? '+' : '-'; + // const absOffset = Math.abs(offsetMinutes); + // const hours = String(Math.floor(absOffset / 60)).padStart(2, '0'); + // const minutes = String(absOffset % 60).padStart(2, '0'); + // const offset = `${sign}${hours}:${minutes}`; + // normalizedDateString = `${dateString}${offset}`; + // } const date = new Date(normalizedDateString); - + // eslint-disable-next-line no-console + console.log(date); if (Number.isNaN(date.getTime())) { throw new TypeError('Invalid date string'); } diff --git a/src/tools/epoch-converter/epoch-converter.vue b/src/tools/epoch-converter/epoch-converter.vue index 0d6c7364..71fba89d 100644 --- a/src/tools/epoch-converter/epoch-converter.vue +++ b/src/tools/epoch-converter/epoch-converter.vue @@ -1,6 +1,6 @@