mirror of
https://github.com/icereed/paperless-gpt.git
synced 2025-03-13 05:08:01 -05:00
Update .gitignore and .dockerignore, enhance component classes, add E2E test setup, and configure Playwright
This commit is contained in:
parent
969bacc137
commit
772034fa8a
17 changed files with 1871 additions and 15 deletions
|
@ -1,2 +1,6 @@
|
||||||
.env
|
.env
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
web-app/e2e
|
||||||
|
web-app/node_modules
|
||||||
|
web-app/playwright_report
|
||||||
|
web-app/test-results
|
70
.github/workflows/e2e-tests.yml
vendored
Normal file
70
.github/workflows/e2e-tests.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: E2E Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./web-app
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: './web-app/package-lock.json'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install chromium --with-deps
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Create test env file
|
||||||
|
run: |
|
||||||
|
echo "PAPERLESS_GPT_URL=http://localhost:8080" > .env.test
|
||||||
|
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env.test
|
||||||
|
|
||||||
|
- name: Start test containers
|
||||||
|
run: npm run docker:test:up
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: npm run test:e2e
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: web-app/playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload test screenshots
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: web-app/test-results/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Stop test containers
|
||||||
|
if: always()
|
||||||
|
run: npm run docker:test:down
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,3 +4,5 @@ prompts/
|
||||||
tests/tmp
|
tests/tmp
|
||||||
tmp/
|
tmp/
|
||||||
db/
|
db/
|
||||||
|
web-app/playwright-report/
|
||||||
|
web-app/test-results/.last-run.json
|
||||||
|
|
8
web-app/.env.test
Normal file
8
web-app/.env.test
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Environment variables for E2E tests
|
||||||
|
PAPERLESS_NGX_URL=http://localhost:8000
|
||||||
|
PAPERLESS_GPT_URL=http://localhost:8080
|
||||||
|
PAPERLESS_ADMIN_USER=admin
|
||||||
|
PAPERLESS_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
# Add your OpenAI API key here (required for document processing)
|
||||||
|
# OPENAI_API_KEY=sk-uhB.... # Replace with your actual OpenAI API key from https://platform.openai.com/api-keys
|
57
web-app/docker-compose.test.yml
Normal file
57
web-app/docker-compose.test.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
paperless-ngx:
|
||||||
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
|
environment:
|
||||||
|
PAPERLESS_URL: http://localhost:8001
|
||||||
|
PAPERLESS_SECRET_KEY: change-me
|
||||||
|
PAPERLESS_ADMIN_USER: admin
|
||||||
|
PAPERLESS_ADMIN_PASSWORD: admin
|
||||||
|
PAPERLESS_TIME_ZONE: Europe/Berlin
|
||||||
|
PAPERLESS_OCR_LANGUAGE: eng
|
||||||
|
PAPERLESS_REDIS: redis://redis:6379
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
volumes:
|
||||||
|
- paperless-data:/usr/src/paperless/data
|
||||||
|
- paperless-media:/usr/src/paperless/media
|
||||||
|
- paperless-export:/usr/src/paperless/export
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: paperless
|
||||||
|
POSTGRES_USER: paperless
|
||||||
|
POSTGRES_PASSWORD: paperless
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
paperless-gpt:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: icereed/paperless-gpt:e2e
|
||||||
|
environment:
|
||||||
|
PAPERLESS_URL: http://paperless-ngx:8000
|
||||||
|
PAPERLESS_ADMIN_USER: admin
|
||||||
|
PAPERLESS_ADMIN_PASSWORD: admin
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- paperless-ngx
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
paperless-data:
|
||||||
|
paperless-media:
|
||||||
|
paperless-export:
|
||||||
|
pgdata:
|
129
web-app/e2e/document-processing.spec.ts
Normal file
129
web-app/e2e/document-processing.spec.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { expect, Page, test } from '@playwright/test';
|
||||||
|
import path, { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { addTagToDocument, PORTS, setupTestEnvironment, TestEnvironment, uploadDocument } from './test-environment';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
let testEnv: TestEnvironment;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
testEnv = await setupTestEnvironment();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await testEnv.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
page = await testEnv.browser.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should process document and show changes in history', async () => {
|
||||||
|
const paperlessNgxPort = testEnv.paperlessNgx.getMappedPort(PORTS.paperlessNgx);
|
||||||
|
const paperlessGptPort = testEnv.paperlessGpt.getMappedPort(PORTS.paperlessGpt);
|
||||||
|
const credentials = { username: 'admin', password: 'admin' };
|
||||||
|
|
||||||
|
// 1. Upload document and add initial tag via API
|
||||||
|
const baseUrl = `http://localhost:${paperlessNgxPort}`;
|
||||||
|
const documentPath = path.join(__dirname, 'fixtures', 'test-document.txt');
|
||||||
|
|
||||||
|
// Get the paperless-gpt tag ID
|
||||||
|
const response = await fetch(`${baseUrl}/api/tags/?name=paperless-gpt`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch paperless-gpt tag');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await response.json();
|
||||||
|
if (!tags.results || tags.results.length === 0) {
|
||||||
|
throw new Error('paperless-gpt tag not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagId = tags.results[0].id;
|
||||||
|
|
||||||
|
// Upload document and get ID
|
||||||
|
const { id: documentId } = await uploadDocument(
|
||||||
|
baseUrl,
|
||||||
|
documentPath,
|
||||||
|
'Original Title',
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Document ID: ${documentId}`);
|
||||||
|
|
||||||
|
// Add tag to document
|
||||||
|
await addTagToDocument(
|
||||||
|
baseUrl,
|
||||||
|
documentId,
|
||||||
|
tagId,
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Navigate to Paperless-GPT UI and process the document
|
||||||
|
await page.goto(`http://localhost:${paperlessGptPort}`);
|
||||||
|
|
||||||
|
// Wait for document to appear in the list
|
||||||
|
await page.waitForSelector('.document-card', { timeout: 1000 * 60 });
|
||||||
|
|
||||||
|
// Click the process button
|
||||||
|
await page.click('button:has-text("Generate Suggestions")');
|
||||||
|
|
||||||
|
// Wait for processing to complete
|
||||||
|
await page.waitForSelector('.suggestions-review', { timeout: 30000 });
|
||||||
|
|
||||||
|
// Apply the suggestions
|
||||||
|
await page.click('button:has-text("Apply")');
|
||||||
|
|
||||||
|
// Wait for success message
|
||||||
|
await page.waitForSelector('.success-modal', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click "OK" on success modal
|
||||||
|
await page.click('button:has-text("OK")');
|
||||||
|
|
||||||
|
// 3. Check history page for the modifications
|
||||||
|
await page.click('a:has-text("History")');
|
||||||
|
|
||||||
|
// Wait for history page to load
|
||||||
|
await page.waitForSelector('.modification-history', { timeout: 5000 });
|
||||||
|
|
||||||
|
// Verify at least one modification entry exists
|
||||||
|
const modifications = await page.locator('.undo-card').count();
|
||||||
|
expect(modifications).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify modification details
|
||||||
|
const firstModification = await page.locator('.undo-card').first();
|
||||||
|
|
||||||
|
// Check if title was modified
|
||||||
|
const titleChange = await firstModification.locator('text=Original Title').isVisible();
|
||||||
|
expect(titleChange).toBeTruthy();
|
||||||
|
|
||||||
|
// Test pagination if there are multiple modifications
|
||||||
|
const paginationVisible = await page.locator('.pagination-controls').isVisible();
|
||||||
|
if (paginationVisible) {
|
||||||
|
// Click next page if available
|
||||||
|
const nextButton = page.locator('button:has-text("Next")');
|
||||||
|
if (await nextButton.isEnabled()) {
|
||||||
|
await nextButton.click();
|
||||||
|
// Wait for new items to load
|
||||||
|
await page.waitForSelector('.undo-card');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Test undo functionality
|
||||||
|
const undoButton = await firstModification.locator('button:has-text("Undo")');
|
||||||
|
if (await undoButton.isEnabled()) {
|
||||||
|
await undoButton.click();
|
||||||
|
// Wait for undo to complete. Text should change to "Undone"
|
||||||
|
await page.waitForSelector('text=Undone');
|
||||||
|
}
|
||||||
|
});
|
20
web-app/e2e/fixtures/test-document.txt
Normal file
20
web-app/e2e/fixtures/test-document.txt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Invoice #12345
|
||||||
|
|
||||||
|
From: Test Company Ltd.
|
||||||
|
To: Sample Client Inc.
|
||||||
|
|
||||||
|
Date: January 27, 2025
|
||||||
|
|
||||||
|
Item Description:
|
||||||
|
1. Software Development Services - $5,000
|
||||||
|
2. Cloud Infrastructure Setup - $2,500
|
||||||
|
3. Technical Consulting - $1,500
|
||||||
|
|
||||||
|
Total: $9,000
|
||||||
|
|
||||||
|
Payment Terms: Net 30
|
||||||
|
Due Date: February 26, 2025
|
||||||
|
|
||||||
|
Please make payment to:
|
||||||
|
Bank: Test Bank
|
||||||
|
Account: 1234567890
|
24
web-app/e2e/setup/global-setup.ts
Normal file
24
web-app/e2e/setup/global-setup.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { chromium } from '@playwright/test';
|
||||||
|
import * as nodeFetch from 'node-fetch';
|
||||||
|
|
||||||
|
// Polyfill fetch for Node.js environment
|
||||||
|
if (!globalThis.fetch) {
|
||||||
|
(globalThis as any).fetch = nodeFetch.default;
|
||||||
|
(globalThis as any).Headers = nodeFetch.Headers;
|
||||||
|
(globalThis as any).Request = nodeFetch.Request;
|
||||||
|
(globalThis as any).Response = nodeFetch.Response;
|
||||||
|
(globalThis as any).FormData = nodeFetch.FormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function globalSetup() {
|
||||||
|
// Install Playwright browser if needed
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
|
console.warn('Warning: OPENAI_API_KEY environment variable is not set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
279
web-app/e2e/test-environment.ts
Normal file
279
web-app/e2e/test-environment.ts
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
import { Browser, chromium } from '@playwright/test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { GenericContainer, Network, StartedTestContainer, Wait } from 'testcontainers';
|
||||||
|
|
||||||
|
export interface TestEnvironment {
|
||||||
|
paperlessNgx: StartedTestContainer;
|
||||||
|
paperlessGpt: StartedTestContainer;
|
||||||
|
browser: Browser;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PORTS = {
|
||||||
|
paperlessNgx: 8000,
|
||||||
|
paperlessGpt: 8080,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function setupTestEnvironment(): Promise<TestEnvironment> {
|
||||||
|
console.log('Setting up test environment...');
|
||||||
|
const paperlessPort = PORTS.paperlessNgx;
|
||||||
|
const gptPort = PORTS.paperlessGpt;
|
||||||
|
|
||||||
|
// Create a network for the containers
|
||||||
|
const network = await new Network().start();
|
||||||
|
|
||||||
|
console.log('Starting Redis container...');
|
||||||
|
const redis = await new GenericContainer('redis:7')
|
||||||
|
.withNetwork(network)
|
||||||
|
.withNetworkAliases('redis')
|
||||||
|
.start();
|
||||||
|
|
||||||
|
console.log('Starting Postgres container...');
|
||||||
|
const postgres = await new GenericContainer('postgres:15')
|
||||||
|
.withNetwork(network)
|
||||||
|
.withNetworkAliases('postgres')
|
||||||
|
.withEnvironment({
|
||||||
|
POSTGRES_DB: 'paperless',
|
||||||
|
POSTGRES_USER: 'paperless',
|
||||||
|
POSTGRES_PASSWORD: 'paperless'
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
|
||||||
|
console.log('Starting Paperless-ngx container...');
|
||||||
|
const paperlessNgx = await new GenericContainer('ghcr.io/paperless-ngx/paperless-ngx:latest')
|
||||||
|
.withNetwork(network)
|
||||||
|
.withNetworkAliases('paperless-ngx')
|
||||||
|
.withEnvironment({
|
||||||
|
PAPERLESS_URL: `http://localhost:${paperlessPort}`,
|
||||||
|
PAPERLESS_SECRET_KEY: 'change-me',
|
||||||
|
PAPERLESS_ADMIN_USER: 'admin',
|
||||||
|
PAPERLESS_ADMIN_PASSWORD: 'admin',
|
||||||
|
PAPERLESS_TIME_ZONE: 'Europe/Berlin',
|
||||||
|
PAPERLESS_OCR_LANGUAGE: 'eng',
|
||||||
|
PAPERLESS_REDIS: 'redis://redis:6379',
|
||||||
|
PAPERLESS_DBHOST: 'postgres',
|
||||||
|
PAPERLESS_DBNAME: 'paperless',
|
||||||
|
PAPERLESS_DBUSER: 'paperless',
|
||||||
|
PAPERLESS_DBPASS: 'paperless'
|
||||||
|
})
|
||||||
|
.withExposedPorts(paperlessPort)
|
||||||
|
.withWaitStrategy(Wait.forHttp('/api/', paperlessPort))
|
||||||
|
.start();
|
||||||
|
|
||||||
|
const mappedPort = paperlessNgx.getMappedPort(paperlessPort);
|
||||||
|
console.log(`Paperless-ngx container started, mapped port: ${mappedPort}`);
|
||||||
|
// Create required tag before starting paperless-gpt
|
||||||
|
const baseUrl = `http://localhost:${mappedPort}`;
|
||||||
|
const credentials = { username: 'admin', password: 'admin' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Creating paperless-gpt tag...');
|
||||||
|
await createTag(baseUrl, 'paperless-gpt', credentials);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create paperless-gpt tag:', error);
|
||||||
|
await paperlessNgx.stop();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting Paperless-gpt container...');
|
||||||
|
const paperlessGpt = await new GenericContainer('icereed/paperless-gpt:e2e')
|
||||||
|
.withNetwork(network)
|
||||||
|
.withEnvironment({
|
||||||
|
PAPERLESS_BASE_URL: `http://paperless-ngx:${paperlessPort}`,
|
||||||
|
PAPERLESS_API_TOKEN: await getApiToken(baseUrl, credentials),
|
||||||
|
LLM_PROVIDER: "openai",
|
||||||
|
LLM_MODEL: "gpt-4",
|
||||||
|
LLM_LANGUAGE: "english",
|
||||||
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
|
||||||
|
})
|
||||||
|
.withExposedPorts(gptPort)
|
||||||
|
.withWaitStrategy(Wait.forHttp('/', gptPort))
|
||||||
|
.start();
|
||||||
|
console.log('Paperless-gpt container started');
|
||||||
|
|
||||||
|
console.log('Launching browser...');
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
console.log('Browser launched');
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
console.log('Cleaning up test environment...');
|
||||||
|
await browser.close();
|
||||||
|
await paperlessGpt.stop();
|
||||||
|
await paperlessNgx.stop();
|
||||||
|
await redis.stop();
|
||||||
|
await postgres.stop();
|
||||||
|
await network.stop();
|
||||||
|
console.log('Test environment cleanup completed');
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Test environment setup completed');
|
||||||
|
return {
|
||||||
|
paperlessNgx,
|
||||||
|
paperlessGpt,
|
||||||
|
browser,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForElement(page: any, selector: string, timeout = 5000): Promise<void> {
|
||||||
|
await page.waitForSelector(selector, { timeout });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaperlessDocument {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
tags: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to upload a document via Paperless-ngx API
|
||||||
|
export async function uploadDocument(
|
||||||
|
baseUrl: string,
|
||||||
|
filePath: string,
|
||||||
|
title: string,
|
||||||
|
credentials: { username: string; password: string }
|
||||||
|
): Promise<PaperlessDocument> {
|
||||||
|
console.log(`Uploading document: ${title} from ${filePath}`);
|
||||||
|
const formData = new FormData();
|
||||||
|
const fileData = await fs.promises.readFile(filePath);
|
||||||
|
formData.append('document', new Blob([fileData]));
|
||||||
|
formData.append('title', title);
|
||||||
|
|
||||||
|
// Initial upload to get task ID
|
||||||
|
const uploadResponse = await fetch(`${baseUrl}/api/documents/post_document/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uploadResponse.ok) {
|
||||||
|
console.error(`Upload failed with status ${uploadResponse.status}: ${uploadResponse.statusText}`);
|
||||||
|
throw new Error(`Failed to upload document: ${uploadResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const task_id = await uploadResponse.json();
|
||||||
|
|
||||||
|
// Poll the tasks endpoint until document is processed
|
||||||
|
while (true) {
|
||||||
|
console.log(`Checking task status for ID: ${task_id}`);
|
||||||
|
const taskResponse = await fetch(`${baseUrl}/api/tasks/?task_id=${task_id}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!taskResponse.ok) {
|
||||||
|
throw new Error(`Failed to check task status: ${taskResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskResultArr = await taskResponse.json();
|
||||||
|
console.log(`Task status: ${JSON.stringify(taskResultArr)}`);
|
||||||
|
|
||||||
|
if (taskResultArr.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const taskResult = taskResultArr[0];
|
||||||
|
// Check if task is completed
|
||||||
|
if (taskResult.status === 'SUCCESS' && taskResult.id) {
|
||||||
|
console.log(`Document processed successfully with ID: ${taskResult.id}`);
|
||||||
|
|
||||||
|
// Fetch the complete document details
|
||||||
|
const documentResponse = await fetch(`${baseUrl}/api/documents/${taskResult.id}/`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!documentResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch document details: ${documentResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await documentResponse.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for failure
|
||||||
|
if (taskResult.status === 'FAILED') {
|
||||||
|
throw new Error(`Document processing failed: ${taskResult.result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before polling again
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Helper to create a tag via Paperless-ngx API
|
||||||
|
export async function createTag(
|
||||||
|
baseUrl: string,
|
||||||
|
name: string,
|
||||||
|
credentials: { username: string; password: string }
|
||||||
|
): Promise<number> {
|
||||||
|
console.log(`Creating tag: ${name}`);
|
||||||
|
const response = await fetch(`${baseUrl}/api/tags/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Tag creation failed with status ${response.status}: ${response.statusText}`);
|
||||||
|
throw new Error(`Failed to create tag: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = await response.json();
|
||||||
|
console.log(`Tag created successfully with ID: ${tag.id}`);
|
||||||
|
return tag.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get an API token
|
||||||
|
export async function getApiToken(
|
||||||
|
baseUrl: string,
|
||||||
|
credentials: { username: string; password: string }
|
||||||
|
): Promise<string> {
|
||||||
|
console.log('Fetching API token');
|
||||||
|
const response = await fetch(`${baseUrl}/api/token/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`API token fetch failed with status ${response.status}: ${response.statusText}`);
|
||||||
|
throw new Error(`Failed to fetch API token: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await response.json();
|
||||||
|
console.log(`API token fetched successfully: ${token.token}`);
|
||||||
|
return token.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to add a tag to a document
|
||||||
|
export async function addTagToDocument(
|
||||||
|
baseUrl: string,
|
||||||
|
documentId: number,
|
||||||
|
tagId: number,
|
||||||
|
credentials: { username: string; password: string }
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Adding tag ${tagId} to document ${documentId}`);
|
||||||
|
const response = await fetch(`${baseUrl}/api/documents/${documentId}/`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
tags: [tagId],
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + btoa(`${credentials.username}:${credentials.password}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Tag addition failed with status ${response.status}: ${response.statusText}`);
|
||||||
|
throw new Error(`Failed to add tag to document: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
console.log('Tag added successfully');
|
||||||
|
}
|
1229
web-app/package-lock.json
generated
1229
web-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,11 @@
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "echo \"TODO\""
|
"pretest:e2e": "docker compose -f docker-compose.test.yml build",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"docker:test:up": "docker compose -f docker-compose.test.yml up -d",
|
||||||
|
"docker:test:down": "docker compose -f docker-compose.test.yml down -v"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.8",
|
"@headlessui/react": "^2.1.8",
|
||||||
|
@ -29,17 +33,21 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
"@types/node": "^22.10.1",
|
"@playwright/test": "^1.50.0",
|
||||||
|
"@types/node": "^22.10.10",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.9",
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
|
"node-fetch": "^3.3.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.12",
|
"tailwindcss": "^3.4.12",
|
||||||
|
"testcontainers": "^10.17.1",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.0.1",
|
"typescript-eslint": "^8.0.1",
|
||||||
"vite": "^5.4.1"
|
"vite": "^5.4.1"
|
||||||
|
|
36
web-app/playwright.config.ts
Normal file
36
web-app/playwright.config.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
// Load test environment variables
|
||||||
|
dotenv.config({ path: '.env.test' });
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
timeout: 120000, // Increased timeout for container startup
|
||||||
|
expect: {
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
globalSetup: './e2e/setup/global-setup.ts',
|
||||||
|
fullyParallel: false,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: 1,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.PAPERLESS_GPT_URL || 'http://localhost:8080',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputDir: 'test-results',
|
||||||
|
preserveOutput: 'failures-only',
|
||||||
|
});
|
|
@ -111,7 +111,7 @@ const History: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="modification-history container mx-auto px-4 py-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||||
Modification History
|
Modification History
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -6,7 +6,7 @@ interface DocumentCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentCard: React.FC<DocumentCardProps> = ({ document }) => (
|
const DocumentCard: React.FC<DocumentCardProps> = ({ document }) => (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-lg shadow-blue-500/50 rounded-md p-4 relative group overflow-hidden">
|
<div className="document-card bg-white dark:bg-gray-800 shadow-lg shadow-blue-500/50 rounded-md p-4 relative group overflow-hidden">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">{document.title}</h3>
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">{document.title}</h3>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2 truncate">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2 truncate">
|
||||||
{document.content.length > 100
|
{document.content.length > 100
|
||||||
|
|
|
@ -11,7 +11,7 @@ const SuccessModal: React.FC<SuccessModalProps> = ({ isOpen, onClose }) => (
|
||||||
<Transition show={isOpen} as={Fragment}>
|
<Transition show={isOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
as="div"
|
as="div"
|
||||||
className="fixed z-10 inset-0 overflow-y-auto"
|
className="success-modal fixed z-10 inset-0 overflow-y-auto"
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const SuggestionsReview: React.FC<SuggestionsReviewProps> = ({
|
||||||
onUpdate,
|
onUpdate,
|
||||||
updating,
|
updating,
|
||||||
}) => (
|
}) => (
|
||||||
<section>
|
<section className="suggestions-review">
|
||||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-6">
|
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-6">
|
||||||
Review and Edit Suggested Titles
|
Review and Edit Suggested Titles
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// UndoCard.tsx
|
// UndoCard.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tooltip } from 'react-tooltip'
|
import { Tooltip } from 'react-tooltip';
|
||||||
|
|
||||||
interface ModificationProps {
|
interface ModificationProps {
|
||||||
ID: number;
|
ID: number;
|
||||||
|
@ -72,7 +72,7 @@ const UndoCard: React.FC<ModificationProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative bg-white dark:bg-gray-800 p-4 rounded-md shadow-md">
|
<div className="undo-card relative bg-white dark:bg-gray-800 p-4 rounded-md shadow-md">
|
||||||
<div className="grid grid-cols-6">
|
<div className="grid grid-cols-6">
|
||||||
<div className="col-span-5"> {/* Left content */}
|
<div className="col-span-5"> {/* Left content */}
|
||||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||||
|
|
Loading…
Reference in a new issue