Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
78bace2
feat(#428): Add non-strategic hearing list types for SIAC, POAC, PAAC…
junaidiqbalmoj Jun 18, 2026
1a0ae8e
fix(#428): Address review feedback for non-strategic publishing
junaidiqbalmoj Jun 19, 2026
4ed8e88
refactor(#428): Consolidate duplicate FTT RPT NJK templates into a si…
junaidiqbalmoj Jun 19, 2026
cd845fd
refactor(#428): Move hardcoded FTT RPT court names into locale files
junaidiqbalmoj Jun 19, 2026
fac2b76
fix(#428): Fix build:views script for ftt-rpt lib
junaidiqbalmoj Jun 19, 2026
29a52bf
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 25, 2026
87316c8
code review refactoring
junaidiqbalmoj Jun 26, 2026
bcf5755
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 26, 2026
b253e48
Fix code review comments
junaidiqbalmoj Jun 26, 2026
f9ae375
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 26, 2026
c92f656
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 29, 2026
adc4874
Merge remote-tracking branch into feature/428-tribunal-non-strategic-…
junaidiqbalmoj Jun 29, 2026
b035fe1
refactor(#428): Consolidate duplicate SIAC/POAC/PAAC NJK templates in…
junaidiqbalmoj Jun 29, 2026
f84461f
remove script not required
junaidiqbalmoj Jun 29, 2026
3ffed41
fix(#428): Update SIAC/POAC/PAAC urlPath to point to consolidated page
junaidiqbalmoj Jun 29, 2026
293d290
fix(#428): Mock @hmcts/azure-blob in pdf-generator tests to prevent u…
junaidiqbalmoj Jun 29, 2026
329a30e
fix(#428): Mock @hmcts/azure-blob in ftt-tax-chamber pdf-generator te…
junaidiqbalmoj Jun 29, 2026
6d5131b
fix(#428): Mock @hmcts/azure-blob in ftt-lrt pdf-generator tests to p…
junaidiqbalmoj Jun 29, 2026
2202932
refactor(#428): Extract shared PDF generation logic into generateFttS…
junaidiqbalmoj Jun 29, 2026
28ed713
fix bugs
junaidiqbalmoj Jul 2, 2026
05aa2ee
merge(#428): Resolve conflicts between feature/428 and master branches
junaidiqbalmoj Jul 3, 2026
416db8f
fix(#428): Update SIAC/FTT renderer tests to match zero-padded date f…
junaidiqbalmoj Jul 3, 2026
fc272c4
chore: sort package.json dependencies alphabetically
junaidiqbalmoj Jul 3, 2026
f03d8d6
fix: restore missing migration SQL files and remove orphan migration …
junaidiqbalmoj Jul 3, 2026
5bee15d
Revert "fix: restore missing migration SQL files and remove orphan mi…
junaidiqbalmoj Jul 3, 2026
f1b27ab
fix bug (add new line for RR text)
junaidiqbalmoj Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/web/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { moduleRoot as civilFamilyCauseListModuleRoot } from "@hmcts/civil-and-f
import { moduleRoot as civilDailyCauseListModuleRoot } from "@hmcts/civil-daily-cause-list/config";
import { moduleRoot as civilAppealModuleRoot } from "@hmcts/court-of-appeal-civil-daily-cause-list/config";
import { moduleRoot as familyDailyCauseListModuleRoot } from "@hmcts/family-daily-cause-list/config";
import { moduleRoot as fttLrtModuleRoot } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list/config";
import { moduleRoot as fttRptModuleRoot } from "@hmcts/ftt-rpt-weekly-hearing-list/config";
import { moduleRoot as fttTaxChamberModuleRoot } from "@hmcts/ftt-tax-chamber-weekly-hearing-list/config";
import { moduleRoot as listTypesCommonModuleRoot } from "@hmcts/list-types-common/config";
import { apiRoutes as locationApiRoutes } from "@hmcts/location/config";
import { moduleRoot as londonAdminModuleRoot } from "@hmcts/london-administrative-court-daily-cause-list/config";
Expand All @@ -19,6 +22,7 @@ import {
moduleRoot as publicPagesModuleRoot
} from "@hmcts/public-pages/config";
import { moduleRoot as rcjStandardModuleRoot } from "@hmcts/rcj-standard-daily-cause-list/config";
import { moduleRoot as siacPoacPaacModuleRoot } from "@hmcts/siac-poac-paac-weekly-hearing-list/config";
import { moduleRoot as sjpPressListModuleRoot } from "@hmcts/sjp-press-list/config";
import { moduleRoot as sjpPublicListModuleRoot } from "@hmcts/sjp-public-list/config";
import { fileUploadRoutes as systemAdminFileUploadRoutes, moduleRoot as systemAdminModuleRoot } from "@hmcts/system-admin-pages/config";
Expand Down Expand Up @@ -92,6 +96,10 @@ export async function createApp(): Promise<Express> {
authModuleRoot,
listTypesCommonModuleRoot,
careStandardsTribunalModuleRoot,
siacPoacPaacModuleRoot,
fttTaxChamberModuleRoot,
fttLrtModuleRoot,
fttRptModuleRoot,
civilFamilyCauseListModuleRoot,
civilDailyCauseListModuleRoot,
familyDailyCauseListModuleRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends "layouts/base-template.njk" %}


{% block page_content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">

<h1 class="govuk-heading-l" id="top">{{ header.listTitle }}</h1>

<p class="govuk-body">
<a href="{{ t.factLinkUrl }}" class="govuk-link">{{ t.factLinkText }}</a> {{ t.factAdditionalText }}
</p>

<p class="govuk-body govuk-!-font-weight-bold govuk-!-margin-bottom-1">{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}</p>
<p class="govuk-body">{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}</p>

<details class="govuk-details govuk-!-margin-top-6" data-module="govuk-details" open>
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
{{ t.importantInformationTitle }}
</span>
</summary>
<div class="govuk-details__text">
<p class="govuk-body">{{ t.importantInformationText }}</p>
<p class="govuk-body">
<a href="{{ t.importantInformationLinkUrl }}" class="govuk-link" target="_blank" rel="noopener noreferrer">
{{ t.importantInformationLinkText }}
</a>
</p>
</div>
</details>

<div class="govuk-form-group govuk-!-margin-top-6">
<h2 class="govuk-heading-s">{{ t.searchCasesTitle }}</h2>
<label class="govuk-label govuk-visually-hidden" for="case-search-input">
{{ t.searchCasesLabel }}
</label>
<input class="govuk-input govuk-!-width-one-half" id="case-search-input" name="search" type="text" aria-label="{{ t.searchCasesLabel }}">
</div>

<div id="hearings-table-container">
<table class="govuk-table" id="hearings-table" role="table" aria-label="{{ header.listTitle }}">
<thead class="govuk-table__head">
<tr class="govuk-table__row">
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.date }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.hearingTime }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.caseName }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.caseReferenceNumber }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.judge }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.venuePlatform }}</th>
</tr>
</thead>
<tbody class="govuk-table__body">
{% for hearing in hearings %}
<tr class="govuk-table__row">
<td class="govuk-table__cell">{{ hearing.date }}</td>
<td class="govuk-table__cell">{{ hearing.hearingTime }}</td>
<td class="govuk-table__cell">{{ hearing.caseName }}</td>
<td class="govuk-table__cell">{{ hearing.caseReferenceNumber }}</td>
<td class="govuk-table__cell">{{ hearing.judge }}</td>
<td class="govuk-table__cell">{{ hearing.venuePlatform }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<p class="govuk-body-s govuk-!-margin-top-6">{{ t.dataSource }}: {{ dataSource }}</p>

<div class="govuk-!-margin-top-6">
<a href="#top" class="govuk-link">{{ t.backToTop }}</a>
</div>

</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { Request, Response } from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockValidate = vi.hoisted(() => vi.fn());

vi.mock("@hmcts/list-types-common", () => ({
createJsonValidator: () => mockValidate,
provenanceLabelsEn: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" },
provenanceLabelsCy: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" }
}));

vi.mock("@hmcts/publication", () => ({
getArtefactById: vi.fn(),
getPublicationJson: vi.fn(),
PROVENANCE_LABELS: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" }
}));

vi.mock("@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list", () => ({
fttLrtWeeklyHearingListEn: {
pageTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List",
provenanceLabels: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" }
},
fttLrtWeeklyHearingListCy: {
pageTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List",
provenanceLabels: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist" }
},
Comment on lines +18 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a distinct Welsh title in the locale mock.

pageTitle is identical in both locale fixtures, so the Welsh test only proves the provenance label changed. If the controller stopped forwarding the Welsh title into renderFttLrtData, this test would still pass. Give the Welsh mock a different title and assert that listTitle matches it. As per coding guidelines, "Implement Welsh language support on all user-facing text with separate en.ts and cy.ts content files".

Also applies to: 121-143

Source: Coding guidelines

renderFttLrtData: vi.fn()
}));

import { renderFttLrtData } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list";
import { getArtefactById, getPublicationJson } from "@hmcts/publication";
import { GET } from "./index.js";

const MOCK_ARTEFACT = {
artefactId: "test-artefact-123",
listTypeId: 32,
contentDate: new Date("2026-01-01"),
lastReceivedDate: new Date("2026-01-01T12:00:00Z"),
provenance: "MANUAL_UPLOAD"
};

const MOCK_JSON_DATA = [
{
date: "01/01/2026",
hearingTime: "10:00am",
caseName: "A Vs B",
caseReferenceNumber: "LRT/00001/2026",
judge: "Judge Smith",
venuePlatform: "London"
}
];

describe("FTT Land Registration Tribunal Weekly Hearing List page controller", () => {
let req: Partial<Request>;
let res: Partial<Response>;

beforeEach(() => {
vi.clearAllMocks();
req = { query: {} };
res = { status: vi.fn().mockReturnThis(), render: vi.fn(), locals: { locale: "en" } };
});

describe("GET handler", () => {
it("should render the list successfully with valid data", async () => {
// Arrange
const mockRenderedData = {
header: {
listTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List",
weekCommencingDate: "1 January 2026",
lastUpdatedDate: "1 January 2026",
lastUpdatedTime: "12pm"
},
hearings: []
};

req.query = { artefactId: "test-artefact-123" };
vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any);
vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA);
mockValidate.mockReturnValue({ isValid: true, errors: [] });
vi.mocked(renderFttLrtData).mockReturnValue(mockRenderedData as any);

// Act
await GET(req as Request, res as Response);

// Assert
expect(getPublicationJson).toHaveBeenCalledWith("test-artefact-123");
expect(renderFttLrtData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ courtName: "First-tier Tribunal (Land Registration Tribunal)" }));
expect(res.render).toHaveBeenCalledWith("ftt-lands-registration-tribunal-weekly-hearing-list", expect.objectContaining({ dataSource: "Manual Upload" }));
});

it("should return 400 when artefactId is missing", async () => {
req.query = {};
await GET(req as Request, res as Response);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Bad Request" }));
});

it("should return 404 when artefact is not found", async () => {
req.query = { artefactId: "non-existent" };
vi.mocked(getArtefactById).mockResolvedValue(null);
await GET(req as Request, res as Response);
expect(res.status).toHaveBeenCalledWith(404);
});

it("should return 404 when blob is not found", async () => {
req.query = { artefactId: "test-artefact-123" };
vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any);
vi.mocked(getPublicationJson).mockResolvedValue(null);
await GET(req as Request, res as Response);
expect(res.status).toHaveBeenCalledWith(404);
});

it("should return 500 on server error", async () => {
req.query = { artefactId: "test-artefact-123" };
vi.mocked(getArtefactById).mockRejectedValue(new Error("Database error"));
await GET(req as Request, res as Response);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Server Error" }));
});

it("should render in Welsh when locale is cy", async () => {
// Arrange
res.locals = { locale: "cy" };
const mockRenderedData = {
header: { listTitle: "title", weekCommencingDate: "", lastUpdatedDate: "", lastUpdatedTime: "" },
hearings: []
};

req.query = { artefactId: "test-artefact-123" };
vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any);
vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA);
mockValidate.mockReturnValue({ isValid: true, errors: [] });
vi.mocked(renderFttLrtData).mockReturnValue(mockRenderedData as any);

// Act
await GET(req as Request, res as Response);

// Assert
expect(renderFttLrtData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ locale: "cy" }));
expect(res.render).toHaveBeenCalledWith(
"ftt-lands-registration-tribunal-weekly-hearing-list",
expect.objectContaining({ dataSource: "Lanlwytho â Llaw" })
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
fttLrtWeeklyHearingListCy as cy,
fttLrtWeeklyHearingListEn as en,
type FttLrtHearingList,
renderFttLrtData
} from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list";
import { schemaPath } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list/config";
import { createJsonValidator } from "@hmcts/list-types-common";
import { createSimpleListTypeHandler, resolveDataSource } from "../list-type-handler.js";

const validate = createJsonValidator(schemaPath);

export const GET = createSimpleListTypeHandler<FttLrtHearingList>({
en,
cy,
validate,
logPrefix: "ftt-lands-registration-tribunal-weekly-hearing-list",
serverError: { errorTitle: "Server Error", errorMessage: "An error occurred while loading the list" },
render: ({ artefact, jsonData, locale, res }) => {
const t = locale === "cy" ? cy : en;

const { header, hearings } = renderFttLrtData(jsonData, {
locale,
courtName: "First-tier Tribunal (Land Registration Tribunal)",
contentDate: artefact.contentDate,
lastReceivedDate: artefact.lastReceivedDate.toISOString(),
listTitle: t.pageTitle
});

const dataSource = resolveDataSource(artefact.provenance, t as { provenanceLabels?: Record<string, string> });

res.render("ftt-lands-registration-tribunal-weekly-hearing-list", { en, cy, t, title: header.listTitle, header, hearings, dataSource });
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{% extends "layouts/base-template.njk" %}


{% block page_content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">

<h1 class="govuk-heading-l" id="top">{{ header.listTitle }}</h1>

<p class="govuk-body">
<a href="{{ t.factLinkUrl }}" class="govuk-link">{{ t.factLinkText }}</a> {{ t.factAdditionalText }}
</p>

<p class="govuk-body govuk-!-font-weight-bold govuk-!-margin-bottom-1">{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}</p>
<p class="govuk-body">{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}</p>

<details class="govuk-details govuk-!-margin-top-6" data-module="govuk-details" open>
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
{{ t.importantInformationTitle }}
</span>
</summary>
<div class="govuk-details__text">
<p class="govuk-body">{{ t.importantInformationText }}</p>
<p class="govuk-body">
<a href="{{ t.importantInformationLinkUrl }}" class="govuk-link" target="_blank" rel="noopener noreferrer">
{{ t.importantInformationLinkText }}
</a>
</p>
</div>
</details>

<div class="govuk-form-group govuk-!-margin-top-6">
<h2 class="govuk-heading-s">{{ t.searchCasesTitle }}</h2>
<label class="govuk-label govuk-visually-hidden" for="case-search-input">
{{ t.searchCasesLabel }}
</label>
<input class="govuk-input govuk-!-width-one-half" id="case-search-input" name="search" type="text" aria-label="{{ t.searchCasesLabel }}">
</div>

<div id="hearings-table-container">
<table class="govuk-table" id="hearings-table" role="table" aria-label="{{ header.listTitle }}">
<thead class="govuk-table__head">
<tr class="govuk-table__row">
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.date }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.time }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.venue }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.caseType }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.caseReferenceNumber }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.judges }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.members }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.hearingMethod }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.additionalInformation }}</th>
</tr>
</thead>
<tbody class="govuk-table__body">
{% for hearing in hearings %}
<tr class="govuk-table__row">
<td class="govuk-table__cell">{{ hearing.date }}</td>
<td class="govuk-table__cell">{{ hearing.time }}</td>
<td class="govuk-table__cell">{{ hearing.venue }}</td>
<td class="govuk-table__cell">{{ hearing.caseType }}</td>
<td class="govuk-table__cell">{{ hearing.caseReferenceNumber }}</td>
<td class="govuk-table__cell">{{ hearing.judges }}</td>
<td class="govuk-table__cell">{{ hearing.members }}</td>
<td class="govuk-table__cell">{{ hearing.hearingMethod }}</td>
<td class="govuk-table__cell">{{ hearing.additionalInformation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<p class="govuk-body-s govuk-!-margin-top-6">{{ t.dataSource }}: {{ dataSource }}</p>

<div class="govuk-!-margin-top-6">
<a href="#top" class="govuk-link">{{ t.backToTop }}</a>
</div>

</div>
</div>
{% endblock %}
Loading
Loading