mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Restructure/frontend editor (#6404)
## Move editor under `frontend/editor/`
Pure restructure: `frontend/` becomes the workspace, `frontend/editor/`
holds
the PDF editor. 1775 file renames + 40 wiring edits. No logic changes.
### Why
`frontend/` is currently the editor — its `src/`, `public/`,
`src-tauri/`,
config files all sit at the root. Promoting `frontend/` to a
workspace and putting the editor in a sibling folder leaves room for
future
apps to drop in alongside it, sharing one `package.json` /
`node_modules` /
lint config / Storybook.
### What moves
frontend/
├── editor/ ← NEW: everything editor-specific
│ ├── src/ ← was frontend/src/
│ ├── public/ ← was frontend/public/
│ ├── src-tauri/ ← was frontend/src-tauri/
│ ├── index.html, vite.config.ts, vitest.config.ts, playwright.config.ts
│ ├── tsconfig*.json, tailwind.config.js, postcss.config.js
│ ├── scripts/
│ ├── .env, .env.desktop, .env.saas
│ └── DeveloperGuide.md
├── package.json, package-lock.json, node_modules/ ← workspace install
├── eslint.config.mjs, .prettierrc, .prettierignore ← shared tooling
├── .gitignore
└── README.md
### Wiring edits (40 files)
- `.taskfiles/frontend.yml`, `desktop.yml`, `e2e.yml`
- `build.gradle`, `app/core/build.gradle`
- `eslint.config.mjs`, `frontend/package.json`, `.gitignore`,
`.prettierignore`
- `docker/frontend/Dockerfile`
- 8 `.github/workflows/*.yml`, plus `.github/dependabot.yml`,
`.github/config/.files.yaml`, `.github/labeler-config-srvaroa.yml`
- `scripts/translations/**`
- Docs: `AGENTS.md`, `CLAUDE.md`, `ADDING_TOOLS.md`,
`DeveloperGuide.md`,
`WINDOWS_SIGNING.md`, `devGuide/HowToAddNewLanguage.md`,
`frontend/README.md`,
`frontend/editor/DeveloperGuide.md`
Plus 3 renamed + edited: `editor/vite.config.ts` (env path +
node_modules
walk-up), `editor/scripts/setup-env.mts` (renamed from `.ts` for
`import.meta.url`), `editor/scripts/build-provisioner.mjs` (resolve
src-tauri
relative to script).
### Verification
| Check | Result |
|---|---|
| `task frontend:typecheck:all` (6 variants) | exit 0 |
| `task frontend:lint` (eslint + dpdm) | exit 0 |
| `task frontend:format:check` | exit 0 |
| `task frontend:test` | 657 tests pass, 50 files |
| `task frontend:build:{core,proprietary,saas,desktop,prototypes}` | all
green |
| `task desktop:build` | full Tauri pipeline →
`Stirling-PDF_2.11.0_x64_en-US.msi` |
| `playwright test --list --project=stubbed` | 172 tests discovered |
`task desktop:build` exercises the heaviest path — Rust + WiX + MSI
bundle
against the moved `editor/src-tauri/`. If anything in the restructure
was
wrong it wouldn't have built.
### Test plan
- [ ] `frontend-validation.yml` green
- [ ] `e2e-stubbed.yml` green
- [ ] `tauri-build.yml` green on at least one platform
- [ ] `check_toml.yml` runs on a translation-touching PR
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
48027ee9d6
commit
0a50e765b7
@@ -0,0 +1,210 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock localStorage for tests
|
||||
class LocalStorageMock implements Storage {
|
||||
private store: Record<string, string> = {};
|
||||
|
||||
get length(): number {
|
||||
return Object.keys(this.store).length;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this.store[key] ?? null;
|
||||
}
|
||||
|
||||
key(index: number): string | null {
|
||||
return Object.keys(this.store)[index] ?? null;
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
delete this.store[key];
|
||||
}
|
||||
|
||||
setItem(key: string, value: string): void {
|
||||
this.store[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: new LocalStorageMock(),
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Mock i18next for tests
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: {
|
||||
changeLanguage: vi.fn(),
|
||||
},
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: "3rdParty",
|
||||
init: vi.fn(),
|
||||
},
|
||||
I18nextProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
// Mock i18next-http-backend
|
||||
vi.mock("i18next-http-backend", () => ({
|
||||
default: {
|
||||
type: "backend",
|
||||
init: vi.fn(),
|
||||
read: vi.fn(),
|
||||
save: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock window.URL.createObjectURL and revokeObjectURL for tests
|
||||
global.URL.createObjectURL = vi.fn(() => "mocked-url");
|
||||
global.URL.revokeObjectURL = vi.fn();
|
||||
|
||||
// Mock File and Blob API methods that aren't available in jsdom
|
||||
if (!globalThis.File.prototype.arrayBuffer) {
|
||||
globalThis.File.prototype.arrayBuffer = function () {
|
||||
// Return a simple ArrayBuffer with some mock data
|
||||
const buffer = new ArrayBuffer(8);
|
||||
const view = new Uint8Array(buffer);
|
||||
view.set([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
return Promise.resolve(buffer);
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.Blob.prototype.arrayBuffer) {
|
||||
globalThis.Blob.prototype.arrayBuffer = function () {
|
||||
// Return a simple ArrayBuffer with some mock data
|
||||
const buffer = new ArrayBuffer(8);
|
||||
const view = new Uint8Array(buffer);
|
||||
view.set([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
return Promise.resolve(buffer);
|
||||
};
|
||||
}
|
||||
|
||||
// Mock crypto.subtle for hashing in tests - force override even if exists
|
||||
const mockHashBuffer = new ArrayBuffer(32);
|
||||
const mockHashView = new Uint8Array(mockHashBuffer);
|
||||
// Fill with predictable mock hash data
|
||||
for (let i = 0; i < 32; i++) {
|
||||
mockHashView[i] = i;
|
||||
}
|
||||
|
||||
// Force override crypto.subtle to avoid Node.js native implementation
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: {
|
||||
subtle: {
|
||||
digest: vi
|
||||
.fn()
|
||||
.mockImplementation(async (_algorithm: string, _data: any) => {
|
||||
// Always return the mock hash buffer regardless of input
|
||||
return mockHashBuffer.slice();
|
||||
}),
|
||||
},
|
||||
getRandomValues: vi.fn().mockImplementation((array: any) => {
|
||||
// Mock getRandomValues if needed
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return array;
|
||||
}),
|
||||
} as unknown as Crypto,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Mock Worker for tests (Web Workers not available in test environment)
|
||||
global.Worker = vi.fn().mockImplementation(() => ({
|
||||
postMessage: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
onmessage: null,
|
||||
onerror: null,
|
||||
}));
|
||||
|
||||
// Mock ResizeObserver for Mantine components
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock IntersectionObserver for components that might use it
|
||||
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock matchMedia for responsive components
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // deprecated
|
||||
removeListener: vi.fn(), // deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Provide a minimal DOMMatrix implementation for pdf.js in the test environment
|
||||
if (typeof globalThis.DOMMatrix === "undefined") {
|
||||
class DOMMatrixStub {
|
||||
a = 1;
|
||||
b = 0;
|
||||
c = 0;
|
||||
d = 1;
|
||||
e = 0;
|
||||
f = 0;
|
||||
|
||||
constructor(init?: string | number[]) {
|
||||
if (Array.isArray(init) && init.length === 6) {
|
||||
[this.a, this.b, this.c, this.d, this.e, this.f] = init as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
multiplySelf(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
translateSelf(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
scaleSelf(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
rotateSelf(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
inverse(): this {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis, "DOMMatrix", {
|
||||
value: DOMMatrixStub,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Set global test timeout to prevent hangs
|
||||
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 });
|
||||
Reference in New Issue
Block a user