Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85ebbd9f65 | |||
| 97d06180b7 |
@@ -1,5 +1,7 @@
|
|||||||
# Current localisation continuation
|
# Current localisation continuation
|
||||||
|
|
||||||
|
Changing the Jobs or Trash display language keeps the existing query results and localises the current error notice. See [the validated request-count comparison](JOBS_LOCALE_READS.md).
|
||||||
|
|
||||||
Admin System database sizes and CV benchmark percentages now follow the EN/NB UI locale on the isolated admin-metrics branch. See [implementation and validation](ADMIN_METRIC_FORMATTING.md).
|
Admin System database sizes and CV benchmark percentages now follow the EN/NB UI locale on the isolated admin-metrics branch. See [implementation and validation](ADMIN_METRIC_FORMATTING.md).
|
||||||
|
|
||||||
Mobile application cards now use the selected EN/NB locale and existing salary-period labels on the isolated mobile-salary branch. See [the bounded implementation and validation](MOBILE_SALARY_FORMATTING.md). Full I18N-02 surface coverage and European locale acceptance remain open.
|
Mobile application cards now use the selected EN/NB locale and existing salary-period labels on the isolated mobile-salary branch. See [the bounded implementation and validation](MOBILE_SALARY_FORMATTING.md). Full I18N-02 surface coverage and European locale acceptance remain open.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Locale-only Jobs and Trash reads
|
||||||
|
|
||||||
|
Bounded PERF-08/PERF-13 and I18N-02 work on `codex/perf-jobs-locale-read`, based on `4c12f7691ff5e73f537135ffed60c0b599c7ce37`. The same Jobs read dependency on translated text was present in the inspected A3 branch.
|
||||||
|
|
||||||
|
Changing the UI language used to reload the same application-list query. The resource loader now tracks data inputs only. The existing error notice renders the current-language title and retains available server details. The shared resource and session hooks, query parameters, retry, data refresh and mutation paths remain intact. This change applies to Jobs and Trash. It does not affect saved language preference writes.
|
||||||
|
|
||||||
|
The three focused assertions reproduced the redundant request before the fix. Afterwards 34 tests passed across JobTable, Offer Comparison, Jobs view and the I18n provider. The tests cover already-loaded mobile salaries and current Jobs and Trash errors switching to Bokmål, followed by an explicit retry of the same query. Full frontend typecheck, scoped ESLint with zero warnings and the Next static export passed.
|
||||||
|
|
||||||
|
## Isolated browser measurement
|
||||||
|
|
||||||
|
Chromium 151.0.7922.34, 1366 × 900, fresh browser context per route, local Next 16.4.1/Turbopack static export, no throttle. Loopback serving disables HTTP caching. Requests to external hosts were blocked. Route interception supplied synthetic API responses, including 15 synthetic application rows and a fixture session. Resource Timing identified loaded JavaScript and CSS. The table reports file byte sizes and offline gzip-6 estimates, excluding HTTP headers and runtime-generated styles.
|
||||||
|
|
||||||
|
| Route | JS files before → after | Raw JS bytes before → after | Estimated gzip-6 JS bytes before → after |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| Landing | 18 → 18 | 2,005,036 → 2,005,009 | 591,192 → 591,190 |
|
||||||
|
| Login | 18 → 18 | 2,005,036 → 2,005,009 | 591,192 → 591,190 |
|
||||||
|
| Jobs | 20 → 20 | 2,033,779 → 2,033,752 | 601,065 → 601,063 |
|
||||||
|
|
||||||
|
All three routes reached their content marker and reported no page errors in both runs. Each loaded one 293-byte CSS file. Jobs made one initial paginated GET in both runs. After switching from English to Bokmål via the storage event, the paginated GET count changed from **one extra request to zero**. The initial Jobs fetch, authentication and other startup requests still occurred. Timing samples were single unthrottled loopback observations, so this checkpoint claims no latency or Web Vitals improvement. Native device, A3 integration and release qualification remain open.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Defer the Jobs and Trash route module
|
||||||
|
|
||||||
|
Bounded PERF-09/PERF-15 experiment on `codex/perf-jobtable-chunk`, based on `97d06180b7f41f8cd9d93c30b373ff3e8eac67b2`. JobTable now loads under the existing route Suspense boundary for Jobs and Trash. Route selection, authentication, localisation, query behaviour and settings ownership are unchanged. The change leaves the shared route loader in charge of loading feedback.
|
||||||
|
|
||||||
|
## Paired cold browser observations
|
||||||
|
|
||||||
|
Three fresh contexts per route on each build, Chromium 151.0.7922.34, 1366 × 900, loopback Next static exports, no throttle, no HTTP cache or service workers, external traffic blocked. The authenticated fixture returned 15 synthetic Jobs. File counts and byte sizes use browser-observed Resource Timing JavaScript entries and the corresponding exported files. Gzip is an offline level-6 estimate. Content time is the first detected visible route marker, including browser automation polling.
|
||||||
|
|
||||||
|
| Route | JS files before → after | Raw JS bytes before → after | Gzip-6 estimate before → after | Content median before → after |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| Landing | 18 → 15 | 2,005,009 → 1,763,692 | 591,190 → 519,804 | 819 → 499 ms |
|
||||||
|
| Login | 18 → 15 | 2,005,009 → 1,763,692 | 591,190 → 519,804 | 404 → 380 ms |
|
||||||
|
| Jobs | 20 → 23 | 2,033,752 → 2,063,063 | 601,063 → 611,639 | 1,014 → 939 ms |
|
||||||
|
|
||||||
|
The public byte reduction was 241,317 raw bytes (12.0%) per route, with a 71,386-byte gzip estimate reduction. Jobs loaded 29,311 more raw bytes and three additional JavaScript files. The first visible Jobs row was detected in 981–1,042 ms before and 928–971 ms after. These small loopback samples showed no first-row regression. A separate functional run reached Jobs in 1,058 ms under concurrent local checks. Controlled mobile/throttled repeats are still needed to understand the additional request waterfall. Timing samples do not establish production latency or Web Vitals.
|
||||||
|
|
||||||
|
All paired routes reached their markers with zero page errors. A separate synthetic browser check exercised keyboard navigation from Jobs to Trash in Bokmål and verified the existing deleted-only query. It also verified that an anonymous Jobs deep link redirects to Landing before requesting application rows. No writes or external calls were allowed. The initial guard probe expected Login as its landing destination and was corrected after source inspection confirmed Landing is the existing contract.
|
||||||
|
|
||||||
|
74 tests passed across route contracts, public auth presentation, Landing redirect, Login, shell navigation and JobTable. Full frontend typecheck, scoped ESLint with zero warnings and the Next static export passed. A3 integration, wider browser/device qualification and full performance acceptance remain open.
|
||||||
@@ -25,7 +25,6 @@ import { ToastProvider } from "./toast";
|
|||||||
import { ConfirmProvider } from "./confirm";
|
import { ConfirmProvider } from "./confirm";
|
||||||
import { PromptProvider } from "./prompt";
|
import { PromptProvider } from "./prompt";
|
||||||
|
|
||||||
import JobTable from "./components/JobTable";
|
|
||||||
import NotificationsPopover from "./components/NotificationsPopover";
|
import NotificationsPopover from "./components/NotificationsPopover";
|
||||||
import type { JobTableColumns } from "./components/JobTable";
|
import type { JobTableColumns } from "./components/JobTable";
|
||||||
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
||||||
@@ -47,6 +46,7 @@ import JobTrackerThemeProvider from "./ThemeProvider";
|
|||||||
import { isSessionInvalidated, SESSION_INVALIDATED_EVENT } from "./sessionBoundary";
|
import { isSessionInvalidated, SESSION_INVALIDATED_EVENT } from "./sessionBoundary";
|
||||||
|
|
||||||
const AddJobModal = lazy(() => import("./components/AddJobModal"));
|
const AddJobModal = lazy(() => import("./components/AddJobModal"));
|
||||||
|
const JobTable = lazy(() => import("./components/JobTable"));
|
||||||
const DashboardView = lazy(() => import("./components/DashboardView"));
|
const DashboardView = lazy(() => import("./components/DashboardView"));
|
||||||
const AnalyticsPage = lazy(() => import("./views/AnalyticsPage"));
|
const AnalyticsPage = lazy(() => import("./views/AnalyticsPage"));
|
||||||
const JobDiscoveryPage = lazy(() => import("./views/JobDiscoveryPage"));
|
const JobDiscoveryPage = lazy(() => import("./views/JobDiscoveryPage"));
|
||||||
|
|||||||
@@ -316,8 +316,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
{
|
{
|
||||||
initialData: { items: [], total: 0, page: 1, pageSize },
|
initialData: { items: [], total: 0, page: 1, pageSize },
|
||||||
enabled: !boardView,
|
enabled: !boardView,
|
||||||
errorMessage: t("jobTableJobsLoadFailed"),
|
errorMessage: "", // ViewStateNotice renders the fallback in the current language.
|
||||||
deps: [params, refreshToken, reloadToken, pageSize, t],
|
deps: [params, refreshToken, reloadToken, pageSize],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -170,11 +170,13 @@ test("mobile salary follows UI language and preserves imported periods and free
|
|||||||
const card = (title: string) => screen.getByRole("link", { name: new RegExp(title, "i") });
|
const card = (title: string) => screen.getByRole("link", { name: new RegExp(title, "i") });
|
||||||
expect(normalize(card(job.jobTitle).textContent)).toContain(salary("en", translations.en.salaryPeriodYear));
|
expect(normalize(card(job.jobTitle).textContent)).toContain(salary("en", translations.en.salaryPeriodYear));
|
||||||
const params = lastParams();
|
const params = lastParams();
|
||||||
|
const readCount = calls().length;
|
||||||
await act(async () => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })); });
|
await act(async () => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })); });
|
||||||
expect(normalize(card(job.jobTitle).textContent)).toContain(salary("nb-NO", translations.nb.salaryPeriodYear));
|
expect(normalize(card(job.jobTitle).textContent)).toContain(salary("nb-NO", translations.nb.salaryPeriodYear));
|
||||||
expect(within(card("Second role")).getByText("0/constructor")).toBeVisible();
|
expect(within(card("Second role")).getByText("0/constructor")).toBeVisible();
|
||||||
expect(within(card("Third role")).getByText("Lønn etter avtale")).toBeVisible();
|
expect(within(card("Third role")).getByText("Lønn etter avtale")).toBeVisible();
|
||||||
expect(lastParams()).toEqual(params);
|
expect(lastParams()).toEqual(params);
|
||||||
|
expect(calls()).toHaveLength(readCount);
|
||||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||||
} finally {
|
} finally {
|
||||||
if (previous) Object.defineProperty(window, "matchMedia", previous);
|
if (previous) Object.defineProperty(window, "matchMedia", previous);
|
||||||
@@ -182,6 +184,28 @@ test("mobile salary follows UI language and preserves imported periods and free
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each(["jobs", "trash"] as const)("language changes localize the current %s read error without a new request", async (mode) => {
|
||||||
|
const title = mode === "jobs" ? "jobTableJobsLoadFailed" : "jobTableTrashLoadFailed";
|
||||||
|
mockedApi.get.mockImplementation((url: string) => url === "/jobapplications"
|
||||||
|
? Promise.reject({ response: { status: 503 } })
|
||||||
|
: Promise.resolve({ data: [job.company] } as any));
|
||||||
|
show(mode, "?q=platform");
|
||||||
|
expect(await screen.findByRole("alert")).toHaveTextContent(translations.en[title]);
|
||||||
|
const readCount = calls().length;
|
||||||
|
const params = lastParams();
|
||||||
|
await act(async () => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" })); });
|
||||||
|
expect(screen.getByRole("alert")).toHaveTextContent(translations.nb[title]);
|
||||||
|
expect(screen.queryByText(translations.en[title])).not.toBeInTheDocument();
|
||||||
|
expect(calls()).toHaveLength(readCount);
|
||||||
|
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
mockedApi.get.mockResolvedValue({ data: { items: [], total: 0, page: 1, pageSize: 15 } } as any);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: translations.nb.retry }));
|
||||||
|
await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument());
|
||||||
|
expect(calls()).toHaveLength(readCount + 1);
|
||||||
|
expect(lastParams()).toEqual(params);
|
||||||
|
});
|
||||||
|
|
||||||
test("partial bulk failure refreshes successful rows and retains only failed visible IDs", async () => {
|
test("partial bulk failure refreshes successful rows and retains only failed visible IDs", async () => {
|
||||||
const other = { ...job, id: 43, jobTitle: "Second role" };
|
const other = { ...job, id: 43, jobTitle: "Second role" };
|
||||||
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies" ? [job.company] : url === "/jobapplications" ? { items: [job, other], total: 2, page: 1, pageSize: 15 } : [] } as any));
|
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies" ? [job.company] : url === "/jobapplications" ? { items: [job, other], total: 2, page: 1, pageSize: 15 } : [] } as any));
|
||||||
|
|||||||
Reference in New Issue
Block a user