Comprehensive test cases for inline scheduling controls on ATS code blocks.
make dev)scheduled_pulse_jobs table-- Ensure pulse tables exist
SELECT name FROM sqlite_master WHERE type='table' AND name='scheduled_pulse_jobs';
-- Clear existing test jobs
DELETE FROM scheduled_pulse_jobs WHERE ats_code LIKE '%test%';
File: web/ts/pulse/types.ts
import { formatInterval } from './types.ts';
// Test cases
expect(formatInterval(30)).toBe('30s');
expect(formatInterval(60)).toBe('1m');
expect(formatInterval(90)).toBe('1m'); // Rounds down
expect(formatInterval(3600)).toBe('1h');
expect(formatInterval(7200)).toBe('2h');
expect(formatInterval(86400)).toBe('1d');
expect(formatInterval(172800)).toBe('2d');
Expected: All intervals format correctly to human-readable strings
import { parseInterval } from './types.ts';
// Valid inputs
expect(parseInterval('30s')).toBe(30);
expect(parseInterval('5m')).toBe(300);
expect(parseInterval('2h')).toBe(7200);
expect(parseInterval('1d')).toBe(86400);
// Invalid inputs
expect(parseInterval('invalid')).toBeNull();
expect(parseInterval('30')).toBeNull(); // Missing unit
expect(parseInterval('s')).toBeNull(); // Missing value
expect(parseInterval('-5m')).toBeNull(); // Negative
Expected: Valid formats parse correctly, invalid formats return null
File: web/ts/pulse/api.ts
import { listScheduledJobs } from './api.ts';
// Setup: Create test jobs via API
// Test
const jobs = await listScheduledJobs();
// Assertions
expect(Array.isArray(jobs)).toBe(true);
expect(jobs.length).toBeGreaterThanOrEqual(0);
if (jobs.length > 0) {
expect(jobs[0]).toHaveProperty('id');
expect(jobs[0]).toHaveProperty('ats_code');
expect(jobs[0]).toHaveProperty('state');
}
Expected: Returns array of ScheduledJob objects
import { createScheduledJob } from './api.ts';
const request = {
ats_code: 'ix https://example.com/test-jobs',
interval_seconds: 3600,
created_from_doc: 'test-doc-123',
};
const job = await createScheduledJob(request);
// Assertions
expect(job.id).toBeTruthy();
expect(job.ats_code).toBe(request.ats_code);
expect(job.interval_seconds).toBe(request.interval_seconds);
expect(job.state).toBe('active');
expect(job.next_run_at).toBeTruthy();
Expected: Returns created job with server-generated ID and timestamps
import { createScheduledJob, pauseScheduledJob, resumeScheduledJob } from './api.ts';
// Create job
const job = await createScheduledJob({
ats_code: 'ix https://example.com/test',
interval_seconds: 3600,
});
// Pause
const pausedJob = await pauseScheduledJob(job.id);
expect(pausedJob.state).toBe('paused');
// Resume
const resumedJob = await resumeScheduledJob(job.id);
expect(resumedJob.state).toBe('active');
Expected: State transitions work correctly
import { createScheduledJob, deleteScheduledJob, getScheduledJob } from './api.ts';
const job = await createScheduledJob({
ats_code: 'ix https://example.com/test',
interval_seconds: 3600,
});
await deleteScheduledJob(job.id);
// Verify deleted (should be inactive state)
const deletedJob = await getScheduledJob(job.id);
expect(deletedJob.state).toBe('inactive');
Expected: Job is set to inactive state (soft delete)
import { getScheduledJob, updateScheduledJob } from './api.ts';
// Non-existent job
try {
await getScheduledJob('nonexistent-id');
fail('Should have thrown error');
} catch (error) {
expect(error.message).toContain('not found');
}
// Invalid update
try {
await updateScheduledJob('nonexistent-id', { state: 'active' });
fail('Should have thrown error');
} catch (error) {
expect(error.message).toBeTruthy();
}
Expected: API errors are properly caught and surfaced
File: web/ts/pulse/scheduling-controls.ts
import { createSchedulingControls } from './scheduling-controls.ts';
const container = createSchedulingControls({
atsCode: 'ix https://example.com/test',
});
// Assertions
expect(container.querySelector('.pulse-btn-add-schedule')).toBeTruthy();
expect(container.querySelector('.pulse-icon')?.textContent).toBe('꩜');
expect(container.textContent).toContain('Add Schedule');
Expected: Renders "Add Schedule" button with pulse icon
import { createSchedulingControls } from './scheduling-controls.ts';
const existingJob = {
id: 'test-job-123',
ats_code: 'ix https://example.com/test',
interval_seconds: 3600,
state: 'active',
// ... other required fields
};
const container = createSchedulingControls({
atsCode: existingJob.ats_code,
existingJob,
});
// Assertions
expect(container.querySelector('.pulse-schedule-badge')).toBeTruthy();
expect(container.querySelector('.pulse-interval')?.textContent).toBe('1h');
expect(container.querySelector('.pulse-state')?.textContent).toBe('active');
expect(container.querySelector('.pulse-btn-pause')).toBeTruthy();
expect(container.querySelector('.pulse-interval-select')).toBeTruthy();
Expected: Renders badge and controls for existing job
import { createSchedulingControls } from './scheduling-controls.ts';
let createdJob = null;
const container = createSchedulingControls({
atsCode: 'ix https://example.com/test',
onJobCreated: (job) => { createdJob = job; },
});
// Click "Add Schedule"
const addBtn = container.querySelector('.pulse-btn-add-schedule');
addBtn.click();
// Verify interval picker appears
expect(container.querySelector('.pulse-interval-picker')).toBeTruthy();
expect(container.querySelector('.pulse-interval-select')).toBeTruthy();
// Select interval
const select = container.querySelector('.pulse-interval-select');
select.value = '3600'; // 1 hour
select.dispatchEvent(new Event('change'));
// Click confirm
const confirmBtn = container.querySelector('.pulse-btn-confirm');
confirmBtn.click();
// Wait for API call
await new Promise(resolve => setTimeout(resolve, 100));
// Assertions
expect(createdJob).toBeTruthy();
expect(createdJob.interval_seconds).toBe(3600);
Expected: User can select interval and create job
File: web/ts/pulse/ats-node-view.ts
import { Schema } from 'prosemirror-model';
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { createATSNodeViewFactory } from './ats-node-view.ts';
// Create test schema with code_block node
const schema = new Schema({
nodes: {
doc: { content: 'block+' },
code_block: {
attrs: { scheduledJobId: { default: null } },
content: 'text*',
},
text: {},
},
});
// Create editor with ATS node view
const state = EditorState.create({
schema,
doc: schema.node('doc', null, [
schema.node('code_block', null, [schema.text('ix https://example.com/test')]),
]),
});
const view = new EditorView(document.createElement('div'), {
state,
nodeViews: {
code_block: createATSNodeViewFactory(),
},
});
// Assertions
const codeBlock = view.dom.querySelector('.ats-code-block-wrapper');
expect(codeBlock).toBeTruthy();
expect(codeBlock.querySelector('.pulse-scheduling-controls')).toBeTruthy();
expect(codeBlock.querySelector('.pulse-btn-add-schedule')).toBeTruthy();
Expected: Node view renders with scheduling controls
// ... setup editor as above
const addBtn = view.dom.querySelector('.pulse-btn-add-schedule');
addBtn.click();
// Select interval and confirm
const select = view.dom.querySelector('.pulse-interval-select');
select.value = '3600';
const confirmBtn = view.dom.querySelector('.pulse-btn-confirm');
confirmBtn.click();
// Wait for API call
await new Promise(resolve => setTimeout(resolve, 500));
// Check that node attributes were updated
const nodePos = 0;
const node = view.state.doc.nodeAt(nodePos);
expect(node.attrs.scheduledJobId).toBeTruthy();
expect(node.attrs.scheduledJobId).toMatch(/^SP/); // ASID format
Expected: Creating schedule updates ProseMirror document attributes
// ... setup editor with existing scheduled job
const deleteBtn = view.dom.querySelector('.pulse-btn-delete');
// Mock confirm dialog
window.confirm = () => true;
deleteBtn.click();
// Wait for API call
await new Promise(resolve => setTimeout(resolve, 500));
// Check that node attribute was removed
const nodePos = 0;
const node = view.state.doc.nodeAt(nodePos);
expect(node.attrs.scheduledJobId).toBeNull();
// Check that UI reverted to "Add Schedule"
expect(view.dom.querySelector('.pulse-btn-add-schedule')).toBeTruthy();
Expected: Deleting schedule removes attribute and reverts UI
Steps:
ix https://example.com/careers꩜ 6h activeExpected: User can create scheduled job through UI
Steps:
꩜ 6h paused꩜ 6h activeExpected: User can toggle job state
Steps:
꩜ 12h activeExpected: User can change interval through UI
Steps:
Expected: User can remove schedule, UI reverts cleanly
Steps:
Expected: Schedule persists across document saves/loads
Steps:
Expected Visual Elements:
#e8f5e9)#2e7d32)Steps:
Expected Visual Elements:
#1b5e20)#a5d6a7)Steps:
Expected: Badge colors reflect state correctly
Steps:
Expected: Icon gently pulses (fades in/out over 2 second cycle)
Steps:
Expected:
Steps:
Expected:
Steps:
Expected: User sees friendly error, can retry successfully
Steps:
Expected: Backend validation prevents creation, error shown to user
Steps:
Expected: Last write wins, or conflict detected and user notified
Steps:
Expected: UI detects job no longer exists, allows recreation
Steps:
Expected: All controls accessible via keyboard
Steps:
Expected:
Steps:
Expected: Clear focus indicators on all interactive elements
Test all functionality in:
Key areas:
Issue: Badge doesn't update after pause/resume Test: Verify badge text and color change immediately
Issue: Node attributes lost on document edits Test: Edit text near scheduled block, verify attributes persist
Issue: Event listeners not cleaned up Test: Create/delete 100 schedules, check memory usage
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'tsx', 'js'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'web/ts/pulse/**/*.ts',
'!web/ts/pulse/**/*.d.ts',
],
};
web/ts/pulse/__tests__/
├── types.test.ts
├── api.test.ts
├── scheduling-controls.test.ts
├── ats-node-view.test.ts
└── integration.test.ts
All tests must pass with: