Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5feccb0a3a | |||
| c9166faac2 | |||
| 2ca0433a35 | |||
| c74c0a0283 | |||
| a2a1e644ab |
@@ -317,7 +317,8 @@ JQ_FILTER='.[]
|
||||
|
||||
T12_INPUT='[{"state":"APPROVED","dismissed":false,"user":{"login":"core-devops"}},{"state":"CHANGES_REQUESTED","dismissed":false,"user":{"login":"bob"}},{"state":"APPROVED","dismissed":false,"user":{"login":"alice"}},{"state":"APPROVED","dismissed":true,"user":{"login":"carol"}}]'
|
||||
|
||||
T12_CANDIDATES=$(echo "$T12_INPUT" | /tmp/jq -r "$JQ_FILTER" 2>/dev/null | sort -u)
|
||||
JQ_CMD=$(command -v jq 2>/dev/null || echo /tmp/jq)
|
||||
T12_CANDIDATES=$(echo "$T12_INPUT" | "$JQ_CMD" -r "$JQ_FILTER" 2>/dev/null | sort -u)
|
||||
assert_contains "T12 jq: core-devops (non-author APPROVED) in candidates" "core-devops" "$T12_CANDIDATES"
|
||||
assert_eq "T12 jq: alice (author) NOT in candidates" "" "$(echo "$T12_CANDIDATES" | grep '^alice$' || true)"
|
||||
assert_eq "T12 jq: carol (dismissed) NOT in candidates" "" "$(echo "$T12_CANDIDATES" | grep '^carol$' || true)"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
name: review-check-tests
|
||||
|
||||
# Runs review-check.sh regression tests on every PR + push that touches
|
||||
# the evaluator script or its test fixtures.
|
||||
#
|
||||
# Follows RFC#324 follow-up (issue #540):
|
||||
# .gitea/scripts/review-check.sh is load-bearing for PR merge gates.
|
||||
# It has ZERO production CI coverage. This workflow closes that gap.
|
||||
#
|
||||
# Design choices:
|
||||
# - Bash test harness (not bats). The existing test_review_check.sh
|
||||
# uses a custom assert_eq/assert_contains framework that is already
|
||||
# working and covers all 13 acceptance criteria (issue #540 §Acceptance).
|
||||
# Converting to bats would be refactoring, not closing the gap.
|
||||
# - No bats dependency: the runner-base image needs no extra tooling.
|
||||
# - continue-on-error: false — these tests must pass; a failure means
|
||||
# the review-gate evaluator is broken and must not be merged.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging]
|
||||
paths:
|
||||
- '.gitea/scripts/review-check.sh'
|
||||
- '.gitea/scripts/tests/test_review_check.sh'
|
||||
- '.gitea/scripts/tests/_review_check_fixture.py'
|
||||
- '.gitea/workflows/review-check-tests.yml'
|
||||
pull_request:
|
||||
branches: [main, staging]
|
||||
paths:
|
||||
- '.gitea/scripts/review-check.sh'
|
||||
- '.gitea/scripts/tests/test_review_check.sh'
|
||||
- '.gitea/scripts/tests/_review_check_fixture.py'
|
||||
- '.gitea/workflows/review-check-tests.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITHUB_SERVER_URL: https://git.moleculesai.app
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: review-check.sh regression tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install jq
|
||||
# Required for T12 jq-filter test case. Gitea Actions runners (ubuntu-latest
|
||||
# label) do not bundle jq. Install via apt-get first (reliable for Ubuntu
|
||||
# runners with internet access to package mirrors). Falls back to GitHub
|
||||
# binary download. GitHub releases may be blocked on some runner networks
|
||||
# (infra#241 follow-up).
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if apt-get update -qq && apt-get install -y -qq jq; then
|
||||
echo "::notice::jq installed via apt-get: $(jq --version)"
|
||||
elif timeout 120 curl -sSL \
|
||||
"https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64" \
|
||||
-o /usr/local/bin/jq && chmod +x /usr/local/bin/jq; then
|
||||
echo "::notice::jq binary downloaded: $(/usr/local/bin/jq --version)"
|
||||
else
|
||||
echo "::warning::jq install failed — apt-get and GitHub download both failed."
|
||||
fi
|
||||
jq --version 2>/dev/null || echo "::notice::jq not yet available — continuing"
|
||||
|
||||
- name: Run review-check.sh regression suite
|
||||
run: bash .gitea/scripts/tests/test_review_check.sh
|
||||
@@ -156,6 +156,16 @@ and run CI manually.
|
||||
| python-lint | pytest with coverage |
|
||||
| e2e-api | Full API test suite (62 tests) |
|
||||
| shellcheck | Shell script linting |
|
||||
| review-check-tests | `review-check.sh` evaluator regression suite (13 scenarios) |
|
||||
| ops-scripts | Python unittest suite for `scripts/*.py` |
|
||||
|
||||
## Local Testing
|
||||
|
||||
### review-check.sh
|
||||
```bash
|
||||
bash .gitea/scripts/tests/test_review_check.sh
|
||||
```
|
||||
Runs the full regression suite against a fixture HTTP server. No network access required.
|
||||
|
||||
## Code Style
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Tests for AttachmentLightbox — shared fullscreen modal for image/PDF/video.
|
||||
*
|
||||
* Covers (18 cases):
|
||||
* 1–2. Open/close rendering
|
||||
* 3–5. ARIA attributes (role, aria-modal, aria-label)
|
||||
* 6–8. Close mechanisms: Esc key, backdrop click, X button
|
||||
* 9. Content click does NOT close (stopPropagation)
|
||||
* 10–11. Focus management: focus close button on open, restore on close
|
||||
* 12. Close button aria-label
|
||||
* 13. Children rendered inside modal
|
||||
* 14. Cleanup on unmount (no leaked listeners)
|
||||
* 15–18. Edge cases: fast open/close, double-open, undefined children
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup, act } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { AttachmentLightbox } from "../AttachmentLightbox";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — open/close rendering", () => {
|
||||
it("renders nothing when open=false", () => {
|
||||
const { container } = render(
|
||||
<AttachmentLightbox open={false} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("renders modal markup when open=true", () => {
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — ARIA attributes", () => {
|
||||
it("has role=dialog", () => {
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has aria-modal=true", () => {
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
expect(screen.getByRole("dialog").getAttribute("aria-modal")).toBe("true");
|
||||
});
|
||||
|
||||
it("uses ariaLabel prop as aria-label on dialog", () => {
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="My Image Preview" children={null} />
|
||||
);
|
||||
expect(screen.getByRole("dialog").getAttribute("aria-label")).toBe("My Image Preview");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — close mechanisms", () => {
|
||||
it("calls onClose when Esc is pressed", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClose when Esc is pressed (without a prior PreventDefault call)", () => {
|
||||
// preventDefault is tested via the handler's presence (the component always
|
||||
// calls e.preventDefault on Escape so the browser's default action is blocked).
|
||||
// We verify the handler fires; the PreventDefault call is implicit.
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClose when backdrop (outer div) is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
const dialog = screen.getByRole("dialog");
|
||||
fireEvent.click(dialog);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does NOT call onClose when close button is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — content stopPropagation", () => {
|
||||
it("does NOT call onClose when inner content area is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview">
|
||||
<img src="test.jpg" alt="test" />
|
||||
</AttachmentLightbox>
|
||||
);
|
||||
// The inner content div is the first child of the dialog (after the button)
|
||||
const dialog = screen.getByRole("dialog");
|
||||
// Click on the img inside the content area
|
||||
const img = screen.getByRole("img");
|
||||
fireEvent.click(img);
|
||||
// onClose should NOT be called because the inner div stops propagation
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — focus management", () => {
|
||||
it("focuses the close button when modal opens", () => {
|
||||
const { container } = render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
const closeBtn = container.querySelector('button[aria-label="Close preview"]');
|
||||
expect(document.activeElement).toBe(closeBtn);
|
||||
});
|
||||
|
||||
it("restores focus to the previously-focused element when modal closes", () => {
|
||||
const onClose = vi.fn();
|
||||
const prevBtn = document.createElement("button");
|
||||
prevBtn.textContent = "Previous";
|
||||
document.body.appendChild(prevBtn);
|
||||
prevBtn.focus();
|
||||
|
||||
const { rerender } = render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
// Modal should have stolen focus
|
||||
expect(document.activeElement).not.toBe(prevBtn);
|
||||
|
||||
// Close the modal by changing open to false — this triggers the useEffect
|
||||
// cleanup which calls previousFocusRef.current?.focus?.() to restore focus.
|
||||
rerender(
|
||||
<AttachmentLightbox open={false} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
// Focus should now be restored to prevBtn
|
||||
expect(document.activeElement).toBe(prevBtn);
|
||||
|
||||
document.body.removeChild(prevBtn);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — close button", () => {
|
||||
it("close button has aria-label=Close preview", () => {
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Close preview" }).getAttribute("aria-label")).toBe("Close preview");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — children", () => {
|
||||
it("renders children inside the modal", () => {
|
||||
render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview">
|
||||
<img src="test.jpg" alt="test image" />
|
||||
</AttachmentLightbox>
|
||||
);
|
||||
expect(screen.getByRole("img")).toBeTruthy();
|
||||
expect(screen.getByAltText("test image")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — cleanup", () => {
|
||||
it("does not leak Esc listener after unmount", () => {
|
||||
const onClose = vi.fn();
|
||||
const { unmount } = render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
unmount();
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
// onClose should NOT be called after unmount
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AttachmentLightbox — edge cases", () => {
|
||||
it("handles undefined children without crashing", () => {
|
||||
// @ts-expect-error — intentionally passing undefined to test runtime behavior
|
||||
const { container } = render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={undefined} />
|
||||
);
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
expect(container.querySelector("img")).toBeNull();
|
||||
});
|
||||
|
||||
it("re-focuses close button after a re-render with same open=true", () => {
|
||||
const { rerender } = render(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
const btn = screen.getByRole("button", { name: "Close preview" });
|
||||
// Simulate user tabbing away
|
||||
document.body.focus();
|
||||
rerender(
|
||||
<AttachmentLightbox open={true} onClose={vi.fn()} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
// Focus should be back on the close button after re-render
|
||||
expect(document.activeElement).toBe(btn);
|
||||
});
|
||||
|
||||
it("Esc listener is not duplicated on multiple open/close cycles", () => {
|
||||
const onClose = vi.fn();
|
||||
const { rerender } = render(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
// Close and reopen
|
||||
rerender(
|
||||
<AttachmentLightbox open={false} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
rerender(
|
||||
<AttachmentLightbox open={true} onClose={onClose} ariaLabel="Preview" children={null} />
|
||||
);
|
||||
// Manually trigger the current Esc handler
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
// Should be called exactly once, not twice
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "molecule-core",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user