test: cover queues and court rotation and document release test choice
This commit is contained in:
1 parent
1d660a3c40
commit
0757e0df76
10 files changed
+730
-39
No files matched your search
@@ -41,6 +41,8 @@ Present the developer with:
|
||||
|
||||
Then explicitly ask the developer to either accept the recommendation or enter a different version. Do not edit versions until they answer.
|
||||
|
||||
During release version confirmation, ask whether to run the test suite or skip it. Running tests is the default; skip them only when the developer explicitly chooses to skip. Record any skipped tests in the release validation report. Lint, build, and Cargo checks still apply.
|
||||
|
||||
Validate the selected value as Semantic Versioning. It should normally be greater than both the current released version and latest release tag. Warn and request confirmation for a lower, equal, or prerelease version.
|
||||
|
||||
Release tags use the selected value prefixed with `v`, such as `v1.2.0`.
|
||||
@@ -72,7 +74,7 @@ After editing `Cargo.toml`, run `cargo check` from `src-tauri/` and confirm Carg
|
||||
|
||||
## Validate
|
||||
|
||||
Activate the required Node.js version, then run the checks in order. Keep `nvm use lts/jod` in the same shell session as the npm commands:
|
||||
Honor the developer's test choice: run the test command by default and omit it only when they explicitly chose to skip. Activate the required Node.js version, then run the checks in order. Keep `nvm use lts/jod` in the same shell session as the npm commands:
|
||||
|
||||
```sh
|
||||
nvm use lts/jod
|
||||
@@ -127,7 +129,7 @@ Report:
|
||||
- recommended bump and reasoning
|
||||
- version selected by the developer
|
||||
- files whose versions were synchronized
|
||||
- test, lint, Angular build, and Cargo results
|
||||
- test, lint, Angular build, and Cargo results, explicitly noting any tests skipped at the developer's request
|
||||
- platform builds not run locally
|
||||
- local release tag created and its target commit
|
||||
- whether the tag remains local or was explicitly pushed
|
||||
|
||||
@@ -112,6 +112,8 @@ For every release, keep the same semantic version in:
|
||||
|
||||
The version displayed in the Settings UI must match these values. Before editing them, inspect Conventional Commits since the latest release tag and recommend the appropriate SemVer bump. If messages are ambiguous, inspect the code/configuration diff since that tag. Present the recommendation and reasoning, allow the developer to enter a different version, and wait for confirmation before changing files. Follow the manual checklist in `docs/development.md`; do not introduce release automation unless requested.
|
||||
|
||||
During release version confirmation, ask whether to run the test suite or skip it. Running tests is the default; skip them only when the developer explicitly chooses to skip. Record any skipped tests in the release validation report. Lint, build, and Cargo checks still apply.
|
||||
|
||||
After the confirmed version is synchronized, committed, and validated, create the matching lightweight local tag with `GIT_EDITOR=true git tag v<version>`. Never tag uncommitted version changes. Do not push the tag without explicit permission because pushing it triggers the release workflow.
|
||||
|
||||
## Validation expectations
|
||||
|
||||
+3
-1
@@ -149,6 +149,8 @@ Use the highest applicable bump. Because older repository commits do not consist
|
||||
|
||||
Show the current version, latest tag, recommendation, bump type, and reasoning to the developer. The developer may accept the recommendation or enter a different semantic version. Do not update files until the developer confirms the final version. Warn before using a version that is not greater than the current release or before using a prerelease identifier.
|
||||
|
||||
During release version confirmation, ask whether to run the test suite or skip it. Running tests is the default; skip them only when the developer explicitly chooses to skip. Record any skipped tests in the release validation report. Lint, build, and Cargo checks still apply.
|
||||
|
||||
### Synchronize the selected version
|
||||
|
||||
After confirmation, set the same semantic version in:
|
||||
@@ -171,7 +173,7 @@ cargo check
|
||||
|
||||
This validates the Rust package and refreshes its lockfile metadata when needed.
|
||||
|
||||
Then verify that all version declarations match and run the checks in `docs/testing.md`.
|
||||
Then verify that all version declarations match and run the checks in `docs/testing.md`, honoring the developer's test choice.
|
||||
|
||||
### Create the release tag
|
||||
|
||||
|
||||
@@ -28,6 +28,22 @@ nvm use lts/jod && npm test -- --configuration=ci
|
||||
|
||||
The test environment requires an available Chrome installation. Test coverage output, when enabled by the test runner, is written under `coverage/app/`.
|
||||
|
||||
Run the suite once in headless Chrome with a coverage report:
|
||||
|
||||
```bash
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
. "$NVM_DIR/nvm.sh"
|
||||
nvm use lts/jod && npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
|
||||
```
|
||||
|
||||
If Chrome is not auto-detected, set `CHROME_BIN` to its executable path
|
||||
(for example, `/snap/bin/chromium`) before running the command. Open
|
||||
`coverage/app/index.html` to inspect coverage by file.
|
||||
|
||||
The queue and assignment service tests exercise real browser IndexedDB, clearing
|
||||
their tables before and after each test. Court rotation page tests use service
|
||||
spies and signals to isolate assignments, confirmation, and role swaps.
|
||||
|
||||
## Lint and build
|
||||
|
||||
Lint TypeScript and Angular templates:
|
||||
|
||||
@@ -1,16 +1,56 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { db } from 'db';
|
||||
import { CourtServiceJudgeService } from './court.service.judge.service';
|
||||
|
||||
describe('CourtServiceJudgeService', () => {
|
||||
describe('CourtServiceJudgeService persistence', () => {
|
||||
let service: CourtServiceJudgeService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
beforeEach(async () => {
|
||||
await db.courtServiceJudges.clear();
|
||||
service = TestBed.inject(CourtServiceJudgeService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await db.courtServiceJudges.clear();
|
||||
});
|
||||
|
||||
it('saves an assignment and returns its generated ID', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 2 });
|
||||
expect(await db.courtServiceJudges.get(id)).toEqual({
|
||||
id,
|
||||
umpireId: 10,
|
||||
courtNo: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an assignment without changing its court', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 2 });
|
||||
expect(await service.update(id, { umpireId: 20 })).toBe(1);
|
||||
expect(await db.courtServiceJudges.get(id)).toEqual({
|
||||
id,
|
||||
umpireId: 20,
|
||||
courtNo: 2
|
||||
});
|
||||
expect(await service.update(id + 100, { umpireId: 30 })).toBe(0);
|
||||
});
|
||||
|
||||
it('deletes only the selected assignment', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 1 });
|
||||
const retained = await service.save({ umpireId: 20, courtNo: 2 });
|
||||
await service.delete(id);
|
||||
expect(await db.courtServiceJudges.toArray()).toEqual([
|
||||
{ id: retained, umpireId: 20, courtNo: 2 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes by official ID while retaining other courts', async () => {
|
||||
await service.save({ umpireId: 10, courtNo: 1 });
|
||||
const id = await service.save({ umpireId: 20, courtNo: 2 });
|
||||
await service.removeByUmpireId(10);
|
||||
await service.removeByUmpireId(999);
|
||||
expect(await db.courtServiceJudges.toArray()).toEqual([
|
||||
{ id, umpireId: 20, courtNo: 2 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,56 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { db } from 'db';
|
||||
import { CourtUmpireService } from './court.umpire.service';
|
||||
|
||||
describe('CourtUmpireService', () => {
|
||||
describe('CourtUmpireService persistence', () => {
|
||||
let service: CourtUmpireService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
beforeEach(async () => {
|
||||
await db.courtUmpires.clear();
|
||||
service = TestBed.inject(CourtUmpireService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await db.courtUmpires.clear();
|
||||
});
|
||||
|
||||
it('saves an assignment and returns its generated ID', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 2 });
|
||||
expect(await db.courtUmpires.get(id)).toEqual({
|
||||
id,
|
||||
umpireId: 10,
|
||||
courtNo: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an assignment without changing its court', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 2 });
|
||||
expect(await service.update(id, { umpireId: 20 })).toBe(1);
|
||||
expect(await db.courtUmpires.get(id)).toEqual({
|
||||
id,
|
||||
umpireId: 20,
|
||||
courtNo: 2
|
||||
});
|
||||
expect(await service.update(id + 100, { umpireId: 30 })).toBe(0);
|
||||
});
|
||||
|
||||
it('deletes only the selected assignment', async () => {
|
||||
const id = await service.save({ umpireId: 10, courtNo: 1 });
|
||||
const retained = await service.save({ umpireId: 20, courtNo: 2 });
|
||||
await service.delete(id);
|
||||
expect(await db.courtUmpires.toArray()).toEqual([
|
||||
{ id: retained, umpireId: 20, courtNo: 2 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes by official ID while retaining other courts', async () => {
|
||||
await service.save({ umpireId: 10, courtNo: 1 });
|
||||
const id = await service.save({ umpireId: 20, courtNo: 2 });
|
||||
await service.removeByUmpireId(10);
|
||||
await service.removeByUmpireId(999);
|
||||
expect(await db.courtUmpires.toArray()).toEqual([
|
||||
{ id, umpireId: 20, courtNo: 2 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,62 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { db } from 'db';
|
||||
import { UmpireService } from './umpire.service';
|
||||
|
||||
describe('Umpire', () => {
|
||||
describe('UmpireService persistence', () => {
|
||||
let service: UmpireService;
|
||||
const official = {
|
||||
firstName: 'Alex',
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: 'M'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
beforeEach(async () => {
|
||||
await db.umpires.clear();
|
||||
service = TestBed.inject(UmpireService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await db.umpires.clear();
|
||||
});
|
||||
|
||||
it('creates and retrieves a complete official record', async () => {
|
||||
const id = await service.create(official);
|
||||
expect(await service.getById(id)).toEqual({ ...official, id });
|
||||
});
|
||||
|
||||
it('updates selected fields while retaining the rest of the record', async () => {
|
||||
const id = await service.create(official);
|
||||
expect(await service.update(id, { country: 'HU' })).toBe(1);
|
||||
expect(await service.getById(id)).toEqual({
|
||||
...official,
|
||||
id,
|
||||
country: 'HU'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined and zero when reading or updating an absent official', async () => {
|
||||
expect(await service.getById(999)).toBeUndefined();
|
||||
expect(await service.update(999, { country: 'HU' })).toBe(0);
|
||||
});
|
||||
|
||||
it('deletes one official and retains the other', async () => {
|
||||
const id = await service.create(official);
|
||||
const retained = await service.create({ ...official, firstName: 'Sam' });
|
||||
await service.delete(id);
|
||||
expect(await service.getById(id)).toBeUndefined();
|
||||
expect(await service.getById(retained)).toEqual({
|
||||
...official,
|
||||
firstName: 'Sam',
|
||||
id: retained
|
||||
});
|
||||
});
|
||||
|
||||
it('clears all officials', async () => {
|
||||
await service.create(official);
|
||||
await service.create({ ...official, firstName: 'Sam' });
|
||||
await service.clear();
|
||||
expect(await db.umpires.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,174 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { db } from 'db';
|
||||
import { WaitingServiceJudgesService } from './waiting-service-judges.service';
|
||||
|
||||
describe('WaitingServiceJudgesService', () => {
|
||||
describe('WaitingServiceJudgesService queue behavior', () => {
|
||||
let service: WaitingServiceJudgesService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
beforeEach(async () => {
|
||||
await db.waitingServiceJudges.clear();
|
||||
await db.umpires.clear();
|
||||
service = TestBed.inject(WaitingServiceJudgesService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await db.waitingServiceJudges.clear();
|
||||
await db.umpires.clear();
|
||||
});
|
||||
|
||||
async function queue() {
|
||||
const items = await db.waitingServiceJudges.orderBy('order').toArray();
|
||||
return items.map((item) => [item.serviceJudgeId, item.order]);
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
await service.add(10);
|
||||
await service.add(20);
|
||||
await service.add(30);
|
||||
}
|
||||
|
||||
it('starts an empty queue at one and appends in FIFO order', async () => {
|
||||
await seed();
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[20, 2],
|
||||
[30, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('appends after the greatest numeric order, regardless of insertion order', async () => {
|
||||
await db.waitingServiceJudges.bulkAdd([
|
||||
{ serviceJudgeId: 10, order: 9 },
|
||||
{ serviceJudgeId: 20, order: 2 }
|
||||
]);
|
||||
await service.add(30);
|
||||
expect(await queue()).toEqual([
|
||||
[20, 2],
|
||||
[10, 9],
|
||||
[30, 10]
|
||||
]);
|
||||
});
|
||||
|
||||
it('serializes concurrent appends with distinct consecutive orders', async () => {
|
||||
await Promise.all([service.add(10), service.add(20), service.add(30)]);
|
||||
const items = await queue();
|
||||
expect(items.map((item) => item[1])).toEqual([1, 2, 3]);
|
||||
expect(items.map((item) => item[0]).sort()).toEqual([10, 20, 30]);
|
||||
});
|
||||
|
||||
it('inserts at a requested position and shifts following officials', async () => {
|
||||
await seed();
|
||||
await service.add(40, 2);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[40, 2],
|
||||
[20, 3],
|
||||
[30, 4]
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves an official toward the front', async () => {
|
||||
await seed();
|
||||
await service.moveToPosition(30, 1);
|
||||
expect(await queue()).toEqual([
|
||||
[30, 1],
|
||||
[10, 2],
|
||||
[20, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves an official toward the back', async () => {
|
||||
await seed();
|
||||
await service.moveToPosition(10, 3);
|
||||
expect(await queue()).toEqual([
|
||||
[20, 1],
|
||||
[30, 2],
|
||||
[10, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves the queue unchanged for a missing official or unchanged position', async () => {
|
||||
await seed();
|
||||
await service.moveToPosition(999, 2);
|
||||
await service.moveToPosition(20, 2);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[20, 2],
|
||||
[30, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes by official ID and closes the gap', async () => {
|
||||
await seed();
|
||||
await service.removeByUmpireId(20);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[30, 2]
|
||||
]);
|
||||
await service.removeByUmpireId(999);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[30, 2]
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes by queue record ID rather than official ID', async () => {
|
||||
await service.add(10);
|
||||
const id = await service.add(20);
|
||||
await service.add(30);
|
||||
await service.remove(id);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[30, 2]
|
||||
]);
|
||||
});
|
||||
|
||||
it('normalizes gaps while preserving numeric ordering', async () => {
|
||||
await db.waitingServiceJudges.bulkAdd([
|
||||
{ serviceJudgeId: 10, order: 12 },
|
||||
{ serviceJudgeId: 20, order: 4 }
|
||||
]);
|
||||
await service.reinitOrders();
|
||||
expect(await queue()).toEqual([
|
||||
[20, 1],
|
||||
[10, 2]
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves the queue head to the complete official record', async () => {
|
||||
const official = {
|
||||
firstName: 'Alex',
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: 'M'
|
||||
};
|
||||
const id = await db.umpires.add(official);
|
||||
await service.add(id);
|
||||
expect(await service.getCurrentUmpire()).toEqual({ ...official, id });
|
||||
});
|
||||
|
||||
it('returns undefined for an empty queue or a missing roster record', async () => {
|
||||
expect(await service.getCurrentUmpire()).toBeUndefined();
|
||||
await service.add(999);
|
||||
expect(await service.getCurrentUmpire()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates only the selected record order', async () => {
|
||||
const id = await service.add(10);
|
||||
await service.add(20);
|
||||
await service.updateOrder(id, 5);
|
||||
expect(await queue()).toEqual([
|
||||
[20, 2],
|
||||
[10, 5]
|
||||
]);
|
||||
});
|
||||
|
||||
it('clears the queue and restarts ordering at one', async () => {
|
||||
await seed();
|
||||
await service.clear();
|
||||
expect(await queue()).toEqual([]);
|
||||
await service.add(40);
|
||||
expect(await queue()).toEqual([[40, 1]]);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,174 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { db } from 'db';
|
||||
import { WaitingUmpiresService } from './waiting-umpires.service';
|
||||
|
||||
describe('WaitingUmpiresService', () => {
|
||||
describe('WaitingUmpiresService queue behavior', () => {
|
||||
let service: WaitingUmpiresService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
beforeEach(async () => {
|
||||
await db.waitingUmpires.clear();
|
||||
await db.umpires.clear();
|
||||
service = TestBed.inject(WaitingUmpiresService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
afterEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await db.waitingUmpires.clear();
|
||||
await db.umpires.clear();
|
||||
});
|
||||
|
||||
async function queue() {
|
||||
const items = await db.waitingUmpires.orderBy('order').toArray();
|
||||
return items.map((item) => [item.umpireId, item.order]);
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
await service.add(10);
|
||||
await service.add(20);
|
||||
await service.add(30);
|
||||
}
|
||||
|
||||
it('starts an empty queue at one and appends in FIFO order', async () => {
|
||||
await seed();
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[20, 2],
|
||||
[30, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('appends after the greatest numeric order, regardless of insertion order', async () => {
|
||||
await db.waitingUmpires.bulkAdd([
|
||||
{ umpireId: 10, order: 9 },
|
||||
{ umpireId: 20, order: 2 }
|
||||
]);
|
||||
await service.add(30);
|
||||
expect(await queue()).toEqual([
|
||||
[20, 2],
|
||||
[10, 9],
|
||||
[30, 10]
|
||||
]);
|
||||
});
|
||||
|
||||
it('serializes concurrent appends with distinct consecutive orders', async () => {
|
||||
await Promise.all([service.add(10), service.add(20), service.add(30)]);
|
||||
const items = await queue();
|
||||
expect(items.map((item) => item[1])).toEqual([1, 2, 3]);
|
||||
expect(items.map((item) => item[0]).sort()).toEqual([10, 20, 30]);
|
||||
});
|
||||
|
||||
it('inserts at a requested position and shifts following officials', async () => {
|
||||
await seed();
|
||||
await service.add(40, 2);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[40, 2],
|
||||
[20, 3],
|
||||
[30, 4]
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves an official toward the front', async () => {
|
||||
await seed();
|
||||
await service.moveToPosition(30, 1);
|
||||
expect(await queue()).toEqual([
|
||||
[30, 1],
|
||||
[10, 2],
|
||||
[20, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves an official toward the back', async () => {
|
||||
await seed();
|
||||
await service.moveToPosition(10, 3);
|
||||
expect(await queue()).toEqual([
|
||||
[20, 1],
|
||||
[30, 2],
|
||||
[10, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves the queue unchanged for a missing official or unchanged position', async () => {
|
||||
await seed();
|
||||
await service.moveToPosition(999, 2);
|
||||
await service.moveToPosition(20, 2);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[20, 2],
|
||||
[30, 3]
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes by official ID and closes the gap', async () => {
|
||||
await seed();
|
||||
await service.removeByUmpireId(20);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[30, 2]
|
||||
]);
|
||||
await service.removeByUmpireId(999);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[30, 2]
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes by queue record ID rather than official ID', async () => {
|
||||
await service.add(10);
|
||||
const id = await service.add(20);
|
||||
await service.add(30);
|
||||
await service.remove(id);
|
||||
expect(await queue()).toEqual([
|
||||
[10, 1],
|
||||
[30, 2]
|
||||
]);
|
||||
});
|
||||
|
||||
it('normalizes gaps while preserving numeric ordering', async () => {
|
||||
await db.waitingUmpires.bulkAdd([
|
||||
{ umpireId: 10, order: 12 },
|
||||
{ umpireId: 20, order: 4 }
|
||||
]);
|
||||
await service.reinitOrders();
|
||||
expect(await queue()).toEqual([
|
||||
[20, 1],
|
||||
[10, 2]
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves the queue head to the complete official record', async () => {
|
||||
const official = {
|
||||
firstName: 'Alex',
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: 'M'
|
||||
};
|
||||
const id = await db.umpires.add(official);
|
||||
await service.add(id);
|
||||
expect(await service.getCurrentUmpire()).toEqual({ ...official, id });
|
||||
});
|
||||
|
||||
it('returns undefined for an empty queue or a missing roster record', async () => {
|
||||
expect(await service.getCurrentUmpire()).toBeUndefined();
|
||||
await service.add(999);
|
||||
expect(await service.getCurrentUmpire()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates only the selected record order', async () => {
|
||||
const id = await service.add(10);
|
||||
await service.add(20);
|
||||
await service.updateOrder(id, 5);
|
||||
expect(await queue()).toEqual([
|
||||
[20, 2],
|
||||
[10, 5]
|
||||
]);
|
||||
});
|
||||
|
||||
it('clears the queue and restarts ordering at one', async () => {
|
||||
await seed();
|
||||
await service.clear();
|
||||
expect(await queue()).toEqual([]);
|
||||
await service.add(40);
|
||||
expect(await queue()).toEqual([[40, 1]]);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,245 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { signal } from '@angular/core';
|
||||
import { AlertController } from '@ionic/angular/standalone';
|
||||
import { CourtServiceJudge, CourtUmpire, Settings, Umpire } from 'db';
|
||||
import { Tab1Page } from './tab1.page';
|
||||
import { SettingsService } from '../services/settings-service';
|
||||
import { UmpireService } from '../services/umpire.service';
|
||||
import { WaitingUmpiresService } from '../services/waiting-umpires.service';
|
||||
import { WaitingServiceJudgesService } from '../services/waiting-service-judges.service';
|
||||
import { CourtUmpireService } from '../services/court.umpire.service';
|
||||
import { CourtServiceJudgeService } from '../services/court.service.judge.service';
|
||||
|
||||
describe('Tab1Page', () => {
|
||||
// Keep page tests independent of IndexedDB and test the public court actions.
|
||||
describe('Tab1Page court rotation', () => {
|
||||
let component: Tab1Page;
|
||||
let fixture: ComponentFixture<Tab1Page>;
|
||||
const official = (id: number): Umpire => ({
|
||||
id,
|
||||
firstName: `Official ${id}`,
|
||||
lastName: 'Smith',
|
||||
country: 'GB',
|
||||
gender: 'M'
|
||||
});
|
||||
const settings = signal<Settings>({
|
||||
id: 1,
|
||||
numberOfCourts: 3,
|
||||
withServiceJudge: true,
|
||||
showAlert: false
|
||||
});
|
||||
const umpires = signal<Umpire[]>([]);
|
||||
const courtUmpires = signal<CourtUmpire[]>([]);
|
||||
const courtJudges = signal<CourtServiceJudge[]>([]);
|
||||
let waitingUmpire: jasmine.SpyObj<WaitingUmpiresService>;
|
||||
let waitingJudge: jasmine.SpyObj<WaitingServiceJudgesService>;
|
||||
let umpireCourt: jasmine.SpyObj<CourtUmpireService>;
|
||||
let judgeCourt: jasmine.SpyObj<CourtServiceJudgeService>;
|
||||
let alerts: jasmine.SpyObj<AlertController>;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
settings.set({
|
||||
id: 1,
|
||||
numberOfCourts: 3,
|
||||
withServiceJudge: true,
|
||||
showAlert: false
|
||||
});
|
||||
umpires.set([
|
||||
official(1),
|
||||
official(2),
|
||||
official(3),
|
||||
official(4),
|
||||
official(5)
|
||||
]);
|
||||
courtUmpires.set([]);
|
||||
courtJudges.set([]);
|
||||
waitingUmpire = jasmine.createSpyObj(
|
||||
'WaitingUmpiresService',
|
||||
['getCurrentUmpire', 'add', 'removeByUmpireId'],
|
||||
{ waitingUmpires: signal([{ id: 1, umpireId: 1, order: 1 }]) }
|
||||
);
|
||||
waitingJudge = jasmine.createSpyObj(
|
||||
'WaitingServiceJudgesService',
|
||||
['getCurrentUmpire', 'add', 'removeByUmpireId'],
|
||||
{ waitingServiceJudges: signal([{ id: 1, serviceJudgeId: 2, order: 1 }]) }
|
||||
);
|
||||
umpireCourt = jasmine.createSpyObj(
|
||||
'CourtUmpireService',
|
||||
['save', 'removeByUmpireId'],
|
||||
{ umpires: courtUmpires }
|
||||
);
|
||||
judgeCourt = jasmine.createSpyObj(
|
||||
'CourtServiceJudgeService',
|
||||
['save', 'removeByUmpireId'],
|
||||
{ umpires: courtJudges }
|
||||
);
|
||||
alerts = jasmine.createSpyObj('AlertController', ['create']);
|
||||
waitingUmpire.getCurrentUmpire.and.resolveTo(official(1));
|
||||
waitingJudge.getCurrentUmpire.and.resolveTo(official(2));
|
||||
waitingUmpire.add.and.resolveTo(1);
|
||||
waitingJudge.add.and.resolveTo(1);
|
||||
umpireCourt.save.and.resolveTo(1);
|
||||
judgeCourt.save.and.resolveTo(1);
|
||||
waitingUmpire.removeByUmpireId.and.resolveTo();
|
||||
waitingJudge.removeByUmpireId.and.resolveTo();
|
||||
umpireCourt.removeByUmpireId.and.resolveTo();
|
||||
judgeCourt.removeByUmpireId.and.resolveTo();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: SettingsService, useValue: { settings } },
|
||||
{ provide: UmpireService, useValue: { umpires } },
|
||||
{ provide: WaitingUmpiresService, useValue: waitingUmpire },
|
||||
{ provide: WaitingServiceJudgesService, useValue: waitingJudge },
|
||||
{ provide: CourtUmpireService, useValue: umpireCourt },
|
||||
{ provide: CourtServiceJudgeService, useValue: judgeCourt },
|
||||
{ provide: AlertController, useValue: alerts }
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(Tab1Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
it('waits for confirmation before assigning officials', async () => {
|
||||
settings.update((value) => ({ ...value, showAlert: true }));
|
||||
const present = jasmine.createSpy('present').and.resolveTo();
|
||||
alerts.create.and.resolveTo({ present } as unknown as HTMLIonAlertElement);
|
||||
await component.showAddConfirmation(2);
|
||||
expect(present).toHaveBeenCalledTimes(1);
|
||||
expect(umpireCourt.save).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
const options = alerts.create.calls.mostRecent().args[0];
|
||||
const confirm = options?.buttons?.find(
|
||||
(button) => typeof button !== 'string' && button.role === 'confirm'
|
||||
);
|
||||
expect(confirm).toBeDefined();
|
||||
if (typeof confirm === 'object' && typeof confirm.handler === 'function') {
|
||||
confirm.handler(undefined);
|
||||
}
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 2,
|
||||
umpireId: 1
|
||||
});
|
||||
expect(judgeCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 2,
|
||||
umpireId: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a lone umpire to the umpire queue after confirming rotation', async () => {
|
||||
settings.update((value) => ({
|
||||
...value,
|
||||
showAlert: true,
|
||||
withServiceJudge: false
|
||||
}));
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
const present = jasmine.createSpy('present').and.resolveTo();
|
||||
alerts.create.and.resolveTo({ present } as unknown as HTMLIonAlertElement);
|
||||
await component.showRemoveConfirmation(2);
|
||||
expect(umpireCourt.removeByUmpireId).not.toHaveBeenCalled();
|
||||
const options = alerts.create.calls.mostRecent().args[0];
|
||||
const confirm = options?.buttons?.find(
|
||||
(button) => typeof button !== 'string' && button.role === 'confirm'
|
||||
);
|
||||
expect(confirm).toBeDefined();
|
||||
if (typeof confirm === 'object' && typeof confirm.handler === 'function') {
|
||||
confirm.handler(undefined);
|
||||
}
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.removeByUmpireId).toHaveBeenCalledOnceWith(3);
|
||||
expect(waitingUmpire.add).toHaveBeenCalledOnceWith(3);
|
||||
expect(waitingJudge.add).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.removeByUmpireId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('assigns both queue heads and removes them from their queues', async () => {
|
||||
await component.showAddConfirmation(3);
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 3,
|
||||
umpireId: 1
|
||||
});
|
||||
expect(judgeCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 3,
|
||||
umpireId: 2
|
||||
});
|
||||
expect(waitingUmpire.removeByUmpireId).toHaveBeenCalledOnceWith(1);
|
||||
expect(waitingJudge.removeByUmpireId).toHaveBeenCalledOnceWith(2);
|
||||
expect(alerts.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not assign either role when the required service judge is missing', async () => {
|
||||
waitingJudge.getCurrentUmpire.and.resolveTo(undefined);
|
||||
await component.showAddConfirmation(1);
|
||||
expect(umpireCourt.save).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
expect(waitingUmpire.removeByUmpireId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not assign when the umpire queue is empty', async () => {
|
||||
waitingUmpire.getCurrentUmpire.and.resolveTo(undefined);
|
||||
await component.showAddConfirmation(1);
|
||||
expect(umpireCourt.save).not.toHaveBeenCalled();
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('assigns only an umpire when service judges are disabled', async () => {
|
||||
settings.update((value) => ({ ...value, withServiceJudge: false }));
|
||||
await component.showAddConfirmation(2);
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.save).toHaveBeenCalledOnceWith({
|
||||
courtNo: 2,
|
||||
umpireId: 1
|
||||
});
|
||||
expect(waitingUmpire.removeByUmpireId).toHaveBeenCalledOnceWith(1);
|
||||
expect(judgeCourt.save).not.toHaveBeenCalled();
|
||||
expect(waitingJudge.removeByUmpireId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('swaps outgoing officials into the opposite role queues', async () => {
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
courtJudges.set([{ id: 1, umpireId: 4, courtNo: 2 }]);
|
||||
await component.showRemoveConfirmation(2);
|
||||
await fixture.whenStable();
|
||||
expect(umpireCourt.removeByUmpireId).toHaveBeenCalledOnceWith(3);
|
||||
expect(judgeCourt.removeByUmpireId).toHaveBeenCalledOnceWith(4);
|
||||
expect(waitingUmpire.add).toHaveBeenCalledOnceWith(4);
|
||||
expect(waitingJudge.add).toHaveBeenCalledOnceWith(3);
|
||||
});
|
||||
|
||||
it('leaves an incomplete court unchanged when service judges are required', async () => {
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
await component.showRemoveConfirmation(2);
|
||||
expect(umpireCourt.removeByUmpireId).not.toHaveBeenCalled();
|
||||
expect(waitingUmpire.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('derives rest from all four duty states and reacts to assignment changes', () => {
|
||||
courtUmpires.set([{ id: 1, umpireId: 3, courtNo: 2 }]);
|
||||
courtJudges.set([{ id: 1, umpireId: 4, courtNo: 2 }]);
|
||||
expect(component.onRest().map((item) => item.id)).toEqual([5]);
|
||||
courtJudges.set([]);
|
||||
expect(component.onRest().map((item) => item.id)).toEqual([4, 5]);
|
||||
});
|
||||
|
||||
it('ignores empty and stale court assignments in its court lookup', () => {
|
||||
courtUmpires.set([
|
||||
{ id: 1, umpireId: null, courtNo: 1 },
|
||||
{ id: 2, umpireId: 999, courtNo: 2 },
|
||||
{ id: 3, umpireId: 3, courtNo: 3 }
|
||||
]);
|
||||
expect([...component.umpireByCourt().entries()]).toEqual([
|
||||
[3, official(3)]
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates court numbers and service judge availability from settings', () => {
|
||||
expect(component.courtIndexes()).toEqual([1, 2, 3]);
|
||||
settings.update((value) => ({
|
||||
...value,
|
||||
numberOfCourts: 1,
|
||||
withServiceJudge: false
|
||||
}));
|
||||
expect(component.courtIndexes()).toEqual([1]);
|
||||
expect(component.serviceJudgeEnabled()).toBeFalse();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user