diff --git a/codecov.yml b/codecov.yml index a92d6a7..3cdf0ac 100644 --- a/codecov.yml +++ b/codecov.yml @@ -17,7 +17,6 @@ ignore: - "vendor/**/*" - "src/main.ts" - "src/editor/editor-frame.ts" - - "src/editor/editor-page.ts" - "src/elpx/elpx-loader.ts" - "src/elpx/service-worker-client.ts" - "src/files/actions.ts" diff --git a/lib/Controller/EditorController.php b/lib/Controller/EditorController.php index 1663315..76810f2 100644 --- a/lib/Controller/EditorController.php +++ b/lib/Controller/EditorController.php @@ -12,26 +12,20 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; -use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\AppFramework\Services\IInitialState; +use OCP\AppFramework\Http\RedirectResponse; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; -use OCP\Util; /** - * Hosts the optional static eXeLearning editor inside a Nextcloud page. The - * editor is only available when its bundle has been downloaded to - * `js/editor/` (run `make download-editor`). - * - * The page itself is a thin shell — the editor is loaded into an iframe via - * `editor-page.ts` using the same postMessage protocol documented by the - * upstream eXeLearning project. + * Backend for the optional static eXeLearning editor, which is only + * available when its bundle has been downloaded to `js/editor/` (run + * `make download-editor`). The view page embeds it through `iframe()` and + * writes the exported package back through `save()`. */ class EditorController extends Controller { public function __construct( @@ -41,76 +35,25 @@ public function __construct( private readonly ElpxPackageService $packageService, private readonly LegacyFileMigrationService $legacyFileMigration, private readonly EditorHtmlService $editorHtml, - private readonly IInitialState $initialState, private readonly IURLGenerator $urlGenerator, ) { parent::__construct($appName, $request); } + /** + * Legacy entry point: the editor now lives inside the view page. Kept as + * a redirect so old bookmarks and links still open the file. + */ #[NoAdminRequired] #[NoCSRFRequired] - public function index(?int $fileId = null, ?string $path = null): TemplateResponse|DataResponse { - $user = $this->userSession->getUser(); - if ($user === null) { - return new DataResponse(['error' => 'Not authenticated'], Http::STATUS_UNAUTHORIZED); - } - - $editorAvailable = is_file(__DIR__ . '/../../js/editor/index.html'); - + public function index(?int $fileId = null, ?string $path = null): RedirectResponse { + $params = ['mode' => 'editor']; if ($fileId !== null && $fileId > 0) { - try { - $file = $this->packageService->getForUserById($user->getUID(), $fileId); - $this->initialState->provideInitialState('file', [ - 'id' => $file->getId(), - 'name' => $file->getName(), - 'path' => $file->getPath(), - 'mtime' => $file->getMTime(), - 'etag' => $file->getEtag(), - 'writable' => $file->isUpdateable(), - ]); - } catch (NotFoundException|NotPermittedException) { - // fall through: editor still opens, just without a preloaded file - } + $params['fileId'] = $fileId; } elseif ($path !== null && $path !== '') { - try { - $file = $this->packageService->getForUserByPath($user->getUID(), $path); - $this->initialState->provideInitialState('file', [ - 'id' => $file->getId(), - 'name' => $file->getName(), - 'path' => $file->getPath(), - 'mtime' => $file->getMTime(), - 'etag' => $file->getEtag(), - 'writable' => $file->isUpdateable(), - ]); - } catch (NotFoundException|NotPermittedException) { - // fall through - } + $params['path'] = $path; } - - // linkTo() returns the right URL whether the app lives at - // /apps// or /custom_apps// (see apps_paths in config). - $editorBasePath = rtrim($this->urlGenerator->linkTo(Application::APP_ID, ''), '/') . '/js/editor'; - // Route URLs always go through Nextcloud's PHP router and live under - // /apps// regardless of apps_paths. - $editorIframeUrl = $this->urlGenerator->linkToRoute(Application::APP_ID . '.editor.iframe'); - - $this->initialState->provideInitialState('editorAvailable', $editorAvailable); - $this->initialState->provideInitialState('editorBasePath', $editorBasePath); - $this->initialState->provideInitialState('editorIframeUrl', $editorIframeUrl); - - Util::addScript(Application::APP_ID, 'exelearning-editor'); - - // RENDER_AS_USER keeps Nextcloud's script + initial-state injection - // working. The template's CSS positions the editor root with - // `position: fixed; inset: 0` so the iframe still uses the whole - // viewport while Nextcloud's chrome stays loaded (but covered). - $response = new TemplateResponse(Application::APP_ID, 'editor', [], TemplateResponse::RENDER_AS_USER); - $csp = new ContentSecurityPolicy(); - $csp->addAllowedScriptDomain("'self'"); - $csp->addAllowedConnectDomain("'self'"); - $csp->addAllowedFrameDomain("'self'"); - $response->setContentSecurityPolicy($csp); - return $response; + return new RedirectResponse($this->urlGenerator->linkToRoute(Application::APP_ID . '.view.index', $params)); } #[NoAdminRequired] diff --git a/lib/Controller/ViewController.php b/lib/Controller/ViewController.php index 87ac8a1..2e62dd4 100644 --- a/lib/Controller/ViewController.php +++ b/lib/Controller/ViewController.php @@ -17,7 +17,6 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IRequest; -use OCP\IURLGenerator; use OCP\IUserSession; use OCP\Util; @@ -38,7 +37,6 @@ public function __construct( private readonly IUserSession $userSession, private readonly ElpxPackageService $packageService, private readonly IInitialState $initialState, - private readonly IURLGenerator $urlGenerator, ) { parent::__construct($appName, $request); } @@ -80,10 +78,6 @@ public function index(?int $fileId = null, ?string $path = null, ?string $mode = 'editorAvailable', is_file(__DIR__ . '/../../js/editor/index.html'), ); - $this->initialState->provideInitialState( - 'editorIframeUrl', - $this->urlGenerator->linkToRoute(Application::APP_ID . '.editor.iframe'), - ); $this->initialState->provideInitialState( 'initialMode', $mode === 'editor' ? 'editor' : 'preview', diff --git a/src/editor/editor-page.ts b/src/editor/editor-page.ts deleted file mode 100644 index 46fd9fe..0000000 --- a/src/editor/editor-page.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Editor-page orchestrator. Loaded by `EditorController` only on the - * `/apps/exelearning/editor` route, never on the Files index. Boots an - * iframe-hosted eXeLearning editor with a file fetched from Nextcloud and - * writes saves back to the same file with an `If-Match` ETag for conflict - * detection. - */ - -import axios from '@nextcloud/axios' -import { loadState } from '@nextcloud/initial-state' -import { generateUrl } from '@nextcloud/router' -import { translate as t } from '@nextcloud/l10n' - -import { loadElpx } from '../elpx/elpx-loader' -import { EditorFrame } from './editor-frame' - -interface InitialFile { - id: number - name: string - path: string - mtime: number - etag: string - writable: boolean -} - -/** - * - */ -async function boot(): Promise { - const root = document.getElementById('exelearning-editor-root') - if (!root) return - - const editorAvailable = safeLoad('editorAvailable', false) - if (!editorAvailable) { - renderMessage(root, t('exelearning', 'The eXeLearning static editor is not installed. Run `make download-editor` in the app directory.')) - return - } - - const file = safeLoad('file', null) - if (!file) { - renderMessage(root, t('exelearning', 'No file selected.')) - return - } - - // Build the iframe URL client-side so it carries the live webroot. The - // server-rendered initial-state value is computed with an empty webroot when - // Nextcloud runs under a sub-path (e.g. the browser Playground), so using it - // as the iframe src would escape the scope and 404. generateUrl() is correct - // in both a normal install and under a scoped path. - const editorIframeUrl = generateUrl('/apps/exelearning/editor/iframe') - - const frame = new EditorFrame(root, { editorIframeUrl }) - - try { - await frame.load() - const loaded = await loadElpx({ fileId: file.id }) - await frame.openFile({ bytes: loaded.bytes, filename: loaded.filename || file.name }) - - window.addEventListener('beforeunload', () => frame.destroy()) - - frame.onMessage(async (message) => { - if (message.type !== 'SAVE_FILE') return - try { - const saved = await frame.requestSave() - const result = await uploadToNextcloud(file, saved.bytes, file.etag) - // EditorController migrates `.elp` → `.elpx` on first save; - // reflect the new name locally so the next save sends the - // updated `file.name` and the document title stops lying. - if (result.name && result.name !== file.name) { - file.name = result.name - document.title = result.name - } - if (result.etag) { - file.etag = result.etag - } - } catch (error) { - // eslint-disable-next-line no-console - console.error('[exelearning] Save failed:', error) - } - }) - } catch (error) { - const detail = error instanceof Error ? error.message : String(error) - renderMessage(root, `${t('exelearning', 'Failed to load the eXeLearning editor.')} — ${detail}`) - } -} - -/** - * Wraps `loadState` so a missing or malformed initial-state entry falls back - * to the supplied default instead of throwing during page boot. - * @param key The initial-state key registered server-side. - * @param fallback Value to return when the key is missing or invalid. - */ -function safeLoad(key: string, fallback: T): T { - try { - return (loadState('exelearning', key, fallback) as T) ?? fallback - } catch { - return fallback - } -} - -/** - * Replaces the target element's content with a single status paragraph. - * Used to surface boot-time errors (missing editor, missing file, …) - * without dragging in a Vue runtime for a single string. - * @param target Element whose content is replaced. - * @param text Localised message to display. - */ -function renderMessage(target: HTMLElement, text: string): void { - target.innerHTML = '' - const p = document.createElement('p') - p.className = 'exelearning-editor__message' - p.textContent = text - target.appendChild(p) -} - -/** - * POSTs the editor's save bytes back to `EditorController::save`. The - * `If-Match` header carries the original ETag so concurrent edits in - * another tab fail loudly with a 412 instead of overwriting silently. - * @param file Metadata for the file being saved (id and display name). - * @param bytes Raw `.elpx` bytes returned by the editor. - * @param ifMatch ETag captured at load time, sent back as `If-Match`. - */ -interface SaveResult { - name?: string - etag?: string -} - -/** - * @param file Metadata for the file being saved (id and display name). - * @param bytes Raw `.elpx` bytes returned by the editor. - * @param ifMatch ETag captured at load time, sent back as `If-Match`. - */ -async function uploadToNextcloud(file: InitialFile, bytes: ArrayBuffer, ifMatch: string): Promise { - const url = generateUrl('/apps/exelearning/editor/save') - const form = new FormData() - form.append('fileId', String(file.id)) - form.append('package', new Blob([bytes], { type: 'application/vnd.exelearning.elpx' }), file.name) - const response = await axios.post(url, form, { - headers: { - 'If-Match': ifMatch, - }, - }) - const data = response.data as SaveResult | undefined - return data ?? {} -} - -void boot() diff --git a/src/elpx/elpx-loader.ts b/src/elpx/elpx-loader.ts index 87de63a..0e5b9f1 100644 --- a/src/elpx/elpx-loader.ts +++ b/src/elpx/elpx-loader.ts @@ -10,22 +10,20 @@ import { generateUrl } from '@nextcloud/router' export interface LoadElpxOptions { fileId?: number path?: string - signal?: AbortSignal } export interface LoadedElpx { bytes: ArrayBuffer filename: string etag?: string - contentLength?: number } /** * Fetches the raw `.elpx` bytes for the current Nextcloud user via * `PackageController`. One of `fileId` or `path` is required; everything - * else (etag, content-length, filename) is best-effort metadata extracted - * from the response headers. - * @param options Lookup options — fileId or path, optional AbortSignal. + * else (etag, filename) is best-effort metadata extracted from the + * response headers. + * @param options Lookup options — fileId or path. */ export async function loadElpx(options: LoadElpxOptions): Promise { if (options.fileId === undefined && (options.path === undefined || options.path === '')) { @@ -39,7 +37,6 @@ export async function loadElpx(options: LoadElpxOptions): Promise { const response = await axios.get(url, { responseType: 'arraybuffer', ...(options.path !== undefined ? { params: { path: options.path } } : {}), - ...(options.signal !== undefined ? { signal: options.signal } : {}), }) const disposition = response.headers['content-disposition'] @@ -51,9 +48,6 @@ export async function loadElpx(options: LoadElpxOptions): Promise { bytes: response.data, filename, ...(typeof etagHeader === 'string' ? { etag: etagHeader } : {}), - ...(typeof response.data.byteLength === 'number' - ? { contentLength: response.data.byteLength } - : {}), } } diff --git a/src/elpx/package-validator.ts b/src/elpx/package-validator.ts index 27c7045..85f0188 100644 --- a/src/elpx/package-validator.ts +++ b/src/elpx/package-validator.ts @@ -15,15 +15,11 @@ */ const ROOT_INDEX_CANDIDATES = ['index.html', 'index.htm'] -const DIR_HINTS = ['html/', 'content/', 'libs/', 'theme/', 'idevices/'] // `contentv3.xml`, `contentv2.foo`, `contentv1.bar`, etc., at root. const LEGACY_MARKER_REGEX = /^contentv\d+(?:\.|$)/ export interface PackageShape { indexEntry: string | null - hasContentXml: boolean - hasScreenshot: boolean - hintCount: number legacyMarker: string | null } @@ -36,10 +32,7 @@ export interface PackageValidation { /** * Walks the decompressed entry map and reports the shape of an eXeLearning - * project: which root index file (if any) is present, whether helper - * artefacts like `content.xml` / `screenshot.png` are there, how many - * familiar directory hints (`html/`, `idevices/`, …) appear, and which - * (if any) `contentv\d+` file at the archive root marks it as a legacy + * project: which root index file (if any) is present, and which (if any) `contentv\d+` file at the archive root marks it as a legacy * pre-`.elpx` project. * @param entries Normalised entry path → bytes from `readPackage()`. */ @@ -51,24 +44,14 @@ export function inspectPackage(entries: ReadonlyMap): Packag break } } - let hintCount = 0 let legacyMarker: string | null = null for (const entry of entries.keys()) { - for (const dir of DIR_HINTS) { - if (entry.startsWith(dir)) { - hintCount += 1 - break - } - } if (legacyMarker === null && LEGACY_MARKER_REGEX.test(entry)) { legacyMarker = entry } } return { indexEntry, - hasContentXml: entries.has('content.xml'), - hasScreenshot: entries.has('screenshot.png'), - hintCount, legacyMarker, } } diff --git a/src/viewer/ViewerToolbar.vue b/src/viewer/ViewerToolbar.vue deleted file mode 100644 index 331c5b4..0000000 --- a/src/viewer/ViewerToolbar.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/templates/editor.php b/templates/editor.php deleted file mode 100644 index 1d124a8..0000000 --- a/templates/editor.php +++ /dev/null @@ -1,42 +0,0 @@ - - -
- -
diff --git a/tests/Unit/Controller/EditorControllerTest.php b/tests/Unit/Controller/EditorControllerTest.php index a88ea8b..a832021 100644 --- a/tests/Unit/Controller/EditorControllerTest.php +++ b/tests/Unit/Controller/EditorControllerTest.php @@ -4,13 +4,11 @@ namespace OCA\ExeLearning\Tests\Unit\Controller; -use OCA\ExeLearning\AppInfo\Application; use OCA\ExeLearning\Controller\EditorController; use OCA\ExeLearning\Service\EditorHtmlService; use OCA\ExeLearning\Service\ElpxPackageService; use OCA\ExeLearning\Service\LegacyFileMigrationService; use OCP\AppFramework\Http; -use OCP\AppFramework\Services\IInitialState; use OCP\Files\File; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; @@ -18,7 +16,6 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; -use OCP\Util; use PHPUnit\Framework\TestCase; final class EditorControllerTest extends TestCase { @@ -27,7 +24,6 @@ final class EditorControllerTest extends TestCase { private ElpxPackageService $packages; private LegacyFileMigrationService $legacyMigration; private EditorHtmlService $editorHtml; - private EditorInitialStateRecorder $initialState; private IURLGenerator $urlGenerator; private EditorController $controller; private string $editorIndexPath; @@ -35,13 +31,11 @@ final class EditorControllerTest extends TestCase { private bool $editorIndexExisted = false; protected function setUp(): void { - Util::reset(); $this->request = $this->createMock(IRequest::class); $this->session = $this->createMock(IUserSession::class); $this->packages = $this->createMock(ElpxPackageService::class); $this->legacyMigration = $this->createMock(LegacyFileMigrationService::class); $this->editorHtml = $this->createMock(EditorHtmlService::class); - $this->initialState = new EditorInitialStateRecorder(); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->urlGenerator->method('linkTo')->willReturn('/custom_apps/exelearning/'); $this->urlGenerator->method('linkToRoute')->willReturn('/apps/exelearning/editor/iframe'); @@ -52,7 +46,6 @@ protected function setUp(): void { $this->packages, $this->legacyMigration, $this->editorHtml, - $this->initialState, $this->urlGenerator, ); $this->editorIndexPath = dirname(__DIR__, 3) . '/js/editor/index.html'; @@ -75,60 +68,39 @@ protected function tearDown(): void { @rmdir(dirname($this->editorIndexPath)); } - public function testIndexRequiresAuthentication(): void { - $this->session->method('getUser')->willReturn(null); - - $response = $this->controller->index(fileId: 42); - - self::assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); - } - - public function testIndexProvidesFileAndEditorStateById(): void { - $this->authenticate(); - $file = $this->packageFile(); - $this->packages->expects(self::once()) - ->method('getForUserById') - ->with('alice', 42) - ->willReturn($file); - - $response = $this->controller->index(fileId: 42); - - self::assertSame(42, $this->initialState->states['file']['id']); - self::assertSame('lesson.elpx', $this->initialState->states['file']['name']); - $this->assertEditorPageState($response); - } - - public function testIndexCanResolveByPathAndIgnoreLookupErrors(): void { - $this->authenticate(); - $file = $this->packageFile(); - $this->packages->expects(self::exactly(3)) - ->method('getForUserByPath') - ->willReturnOnConsecutiveCalls( - $file, - self::throwException(new NotFoundException('missing')), - self::throwException(new NotPermittedException('denied')), - ); - - self::assertSame(Http::STATUS_OK, $this->controller->index(path: 'lesson.elpx')->getStatus()); - $this->initialState->states = []; - Util::reset(); - self::assertSame(Http::STATUS_OK, $this->controller->index(path: 'missing.elpx')->getStatus()); - $this->initialState->states = []; - Util::reset(); - self::assertSame(Http::STATUS_OK, $this->controller->index(path: 'denied.elpx')->getStatus()); - } + /** + * @return iterable, array}> + */ + public static function legacyIndexLinks(): iterable { + yield 'by id' => [['fileId' => 42], ['mode' => 'editor', 'fileId' => 42]]; + yield 'by path' => [['path' => 'lesson.elpx'], ['mode' => 'editor', 'path' => 'lesson.elpx']]; + yield 'bare' => [[], ['mode' => 'editor']]; + } + + /** + * @param array $args + * @param array $expectedParams + * @dataProvider legacyIndexLinks + */ + public function testIndexRedirectsLegacyLinksToTheViewPage(array $args, array $expectedParams): void { + $urlGenerator = $this->createMock(IURLGenerator::class); + $urlGenerator->expects(self::once()) + ->method('linkToRoute') + ->with('exelearning.view.index', $expectedParams) + ->willReturn('/apps/exelearning/view?mode=editor'); + $controller = new EditorController( + 'exelearning', + $this->request, + $this->session, + $this->packages, + $this->legacyMigration, + $this->editorHtml, + $urlGenerator, + ); - public function testIndexIgnoresIdLookupErrors(): void { - $this->authenticate(); - $this->packages->expects(self::exactly(2)) - ->method('getForUserById') - ->willReturnOnConsecutiveCalls( - self::throwException(new NotFoundException('missing')), - self::throwException(new NotPermittedException('denied')), - ); + $response = $controller->index(...$args); - self::assertSame(Http::STATUS_OK, $this->controller->index(fileId: 42)->getStatus()); - self::assertSame(Http::STATUS_OK, $this->controller->index(fileId: 43)->getStatus()); + self::assertSame('/apps/exelearning/view?mode=editor', $response->getRedirectURL()); } public function testSaveRequiresAuthentication(): void { @@ -217,19 +189,6 @@ private function packageFile(bool $updateable = true): File { return $file; } - private function assertEditorPageState(object $response): void { - self::assertSame(Http::STATUS_OK, $response->getStatus()); - self::assertSame(false, $this->initialState->states['editorAvailable']); - self::assertSame('/custom_apps/exelearning/js/editor', $this->initialState->states['editorBasePath']); - self::assertSame('/apps/exelearning/editor/iframe', $this->initialState->states['editorIframeUrl']); - self::assertSame([[Application::APP_ID, 'exelearning-editor']], Util::$scripts); - $policy = $response->getContentSecurityPolicy(); - self::assertNotNull($policy); - self::assertSame(["'self'"], $policy->scriptDomains); - self::assertSame(["'self'"], $policy->connectDomains); - self::assertSame(["'self'"], $policy->frameDomains); - } - private function writeEditorIndex(string $html): void { $dir = dirname($this->editorIndexPath); if (!is_dir($dir)) { @@ -238,12 +197,3 @@ private function writeEditorIndex(string $html): void { file_put_contents($this->editorIndexPath, $html); } } - -final class EditorInitialStateRecorder implements IInitialState { - /** @var array */ - public array $states = []; - - public function provideInitialState(string $key, mixed $value): void { - $this->states[$key] = $value; - } -} diff --git a/tests/Unit/Controller/ViewControllerTest.php b/tests/Unit/Controller/ViewControllerTest.php index 47ae3fc..136738b 100644 --- a/tests/Unit/Controller/ViewControllerTest.php +++ b/tests/Unit/Controller/ViewControllerTest.php @@ -13,7 +13,6 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IRequest; -use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; use OCP\Util; @@ -23,7 +22,6 @@ final class ViewControllerTest extends TestCase { private IUserSession $session; private ElpxPackageService $packages; private InitialStateRecorder $initialState; - private IURLGenerator $urlGenerator; private ViewController $controller; protected function setUp(): void { @@ -31,16 +29,12 @@ protected function setUp(): void { $this->session = $this->createMock(IUserSession::class); $this->packages = $this->createMock(ElpxPackageService::class); $this->initialState = new InitialStateRecorder(); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->urlGenerator->method('linkToRoute') - ->willReturn('/apps/exelearning/editor/iframe'); $this->controller = new ViewController( 'exelearning', $this->createMock(IRequest::class), $this->session, $this->packages, $this->initialState, - $this->urlGenerator, ); } @@ -139,7 +133,6 @@ private function packageFile(): File { private function assertPageStateAndPolicy(object $response, string $mode): void { self::assertSame(Http::STATUS_OK, $response->getStatus()); self::assertSame(false, $this->initialState->states['editorAvailable']); - self::assertSame('/apps/exelearning/editor/iframe', $this->initialState->states['editorIframeUrl']); self::assertSame($mode, $this->initialState->states['initialMode']); self::assertSame([[Application::APP_ID, 'exelearning-view']], Util::$scripts); diff --git a/tests/bootstrap-standalone.php b/tests/bootstrap-standalone.php index 4c46162..f4e8d9d 100644 --- a/tests/bootstrap-standalone.php +++ b/tests/bootstrap-standalone.php @@ -143,6 +143,10 @@ public function __construct( public function setContentSecurityPolicy(ContentSecurityPolicy $policy): void { $this->contentSecurityPolicy = $policy; } public function getContentSecurityPolicy(): ?ContentSecurityPolicy { return $this->contentSecurityPolicy; } } + class RedirectResponse extends DataResponse { + public function __construct(private string $redirectURL) { parent::__construct(null, 303); } + public function getRedirectURL(): string { return $this->redirectURL; } + } class StreamResponse extends DataResponse { public function __construct($stream) { parent::__construct($stream, 200); } public function getStream() { return $this->data; } diff --git a/tests/js/package-validator.test.ts b/tests/js/package-validator.test.ts index 920be08..c8d6acf 100644 --- a/tests/js/package-validator.test.ts +++ b/tests/js/package-validator.test.ts @@ -21,9 +21,6 @@ describe('inspectPackage', () => { 'idevices/widget.js', )) expect(shape.indexEntry).toBe('index.html') - expect(shape.hasContentXml).toBe(true) - expect(shape.hasScreenshot).toBe(true) - expect(shape.hintCount).toBe(4) expect(shape.legacyMarker).toBeNull() }) diff --git a/vite.config.mts b/vite.config.mts index 83fc026..b4f4245 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -34,7 +34,6 @@ function cleanGeneratedJs(): Plugin { export default createAppConfig( { main: resolve('src', 'main.ts'), - editor: resolve('src', 'editor', 'editor-page.ts'), view: resolve('src', 'view', 'view-page.ts'), }, {