Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ npm run build:check
# Clean dist directory only
npm run clean

# Run tests
# Run unit tests (Vitest + jsdom)
npm test

# Run tests against jQuery 4
# Run unit tests against jQuery 4
npm run test:jquery4

# Run e2e tests (Playwright, requires build first)
npm run test:e2e
```

## Architecture
Expand Down Expand Up @@ -61,7 +64,8 @@ demo/
demo.css - Shared demo page styles
fixtures/ - Mock AJAX endpoint files (get.json, save.json, delete.json)
images/ - Demo-only images
test/ - Vitest test suite
test/ - Vitest unit test suite (jsdom)
e2e/ - Playwright e2e tests (one spec per demo page)
dist/ - Built output (gitignored)
docs/ - Migration plans and design documents
```
Expand Down
53 changes: 53 additions & 0 deletions e2e/programmatic-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,59 @@ test.describe('Programmatic API', () => {
await expect(areas).toHaveCount(4);
});

test('Clear All removes manually added annotation', async ({ page }) => {
// Click "Add Note" button in the plugin controls
await page.click('#btn-add');

// Draw a rectangle on the image by dragging on the edit overlay
const editArea = page.locator('.image-annotate-edit-area');
await expect(editArea).toBeVisible();

// Type text and save
const textarea = page.locator('.image-annotate-edit-form textarea');
await textarea.fill('Manual test note');
await page.click('.image-annotate-edit-ok');

// Should now have 5 annotations (4 initial + 1 manual)
await expect(page.locator('.image-annotate-area')).toHaveCount(5);

// Clear all
await page.click('#btn-clear');

// ALL annotations should be gone, including the manually added one
await expect(page.locator('.image-annotate-area')).toHaveCount(0);
});

test('Reload Notes after manual add does not leave orphaned views', async ({ page }) => {
// Add a manual note
await page.click('#btn-add');
const textarea = page.locator('.image-annotate-edit-form textarea');
await textarea.fill('Will be orphaned');
await page.click('.image-annotate-edit-ok');

await expect(page.locator('.image-annotate-area')).toHaveCount(5);

// Reload notes — should replace all views with just the 4 initial ones
await page.click('#btn-reload');
await expect(page.locator('.image-annotate-area')).toHaveCount(4);
});

test('Export after manual add includes the manually added note', async ({ page }) => {
// Add a manual note
await page.click('#btn-add');
const textarea = page.locator('.image-annotate-edit-form textarea');
await textarea.fill('Exported note');
await page.click('.image-annotate-edit-ok');

// Export
await page.click('#btn-export');
const exportText = await page.locator('#export-target').textContent();
const notes = JSON.parse(exportText!);

expect(notes).toHaveLength(5);
expect(notes.some((n: { text: string }) => n.text === 'Exported note')).toBe(true);
});

test('status log records operations', async ({ page }) => {
const status = page.locator('#api-status');
await expect(status).toContainText('initialized');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "annotate-image",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"description": "Create Flickr-like comment annotations on images — draw rectangles, add notes, save via AJAX or static data",
"license": "GPL-2.0",
"repository": {
Expand Down
1 change: 1 addition & 0 deletions src/annotate-edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export class AnnotateEdit {
} else {
this.note.editable = true;
const view = new AnnotateView(this.image, this.note);
this.note.view = view;
view.resetPosition(this, text);
this.image.notes.push(this.note);
}
Expand Down
2 changes: 2 additions & 0 deletions src/annotate-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export class AnnotateImage {
for (const note of this.notes) {
note.view?.destroy();
}
// Remove any orphaned view elements (e.g. if notes array was replaced externally)
this.viewOverlay.replaceChildren();
}

private createViews(): void {
Expand Down
49 changes: 49 additions & 0 deletions test/annotate-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,55 @@ describe('annotateEdit — save new annotation', () => {
expect(inst.viewOverlay.querySelector('.image-annotate-note').textContent).toBe('Saved note');
});

test('save assigns view to the note so destroyViews can clean it up', () => {
const image = createTestImage();
const inst = getInstance(image);

inst.add();
inst.canvas.querySelector('.image-annotate-edit-form textarea').value = 'Test';
inst.canvas.querySelector('.image-annotate-edit-ok').click();

expect(inst.notes[0].view).toBeDefined();
});

test('clear() removes DOM of manually added note', () => {
const image = createTestImage();
const inst = getInstance(image);

// Add a note via the UI flow (add + save)
inst.add();
inst.canvas.querySelector('.image-annotate-edit-form textarea').value = 'Manual note';
inst.canvas.querySelector('.image-annotate-edit-ok').click();

expect(inst.viewOverlay.querySelectorAll('.image-annotate-area').length).toBe(1);

// Clear all notes
inst.clear();

expect(inst.notes.length).toBe(0);
expect(inst.viewOverlay.querySelectorAll('.image-annotate-area').length).toBe(0);
});

test('load() after manual add replaces all views without orphans', () => {
const image = createTestImage();
const inst = getInstance(image);

// Add a note via the UI flow (add + save)
inst.add();
inst.canvas.querySelector('.image-annotate-edit-form textarea').value = 'Manual note';
inst.canvas.querySelector('.image-annotate-edit-ok').click();

expect(inst.viewOverlay.querySelectorAll('.image-annotate-area').length).toBe(1);

// Replace notes and reload
inst.notes = [{ id: 'r1', top: 10, left: 10, width: 50, height: 50, text: 'Reloaded', editable: true }];
inst.load();

// Should have exactly one view — the reloaded note, not the orphaned manual one
expect(inst.viewOverlay.querySelectorAll('.image-annotate-area').length).toBe(1);
expect(inst.viewOverlay.querySelector('.image-annotate-note').textContent).toBe('Reloaded');
});

test('save with api.save sends JSON to callback', async () => {
const saveFn = vi.fn(() => Promise.resolve({ annotation_id: 'new-42' }));

Expand Down
Loading