From 90a25be99e54701f20e9ef17aaf1b82ac135a24d Mon Sep 17 00:00:00 2001 From: erseco Date: Sat, 26 Sep 2026 07:04:54 +0100 Subject: [PATCH] refactor: drop the unreachable standalone editor page and dead fields Nothing in the UI links to /apps/exelearning/editor any more; file actions and the New menu open /view?mode=editor. Keep the route as a redirect there so old links still work, and delete the page's template, its editor-page.ts entry (which also carried an unfiltered SAVE_FILE handler that re-requested saves on every reply) and its initial state. Also remove ViewerToolbar.vue (never imported), the view page's unused editorIframeUrl initial state (the client builds it with the live webroot), and PackageShape/LoadedElpx fields nothing reads. --- codecov.yml | 1 - lib/Controller/EditorController.php | 85 ++-------- lib/Controller/ViewController.php | 6 - src/editor/editor-page.ts | 148 ------------------ src/elpx/elpx-loader.ts | 12 +- src/elpx/package-validator.ts | 19 +-- src/viewer/ViewerToolbar.vue | 39 ----- templates/editor.php | 42 ----- .../Unit/Controller/EditorControllerTest.php | 112 ++++--------- tests/Unit/Controller/ViewControllerTest.php | 7 - tests/bootstrap-standalone.php | 4 + tests/js/package-validator.test.ts | 3 - vite.config.mts | 1 - 13 files changed, 53 insertions(+), 426 deletions(-) delete mode 100644 src/editor/editor-page.ts delete mode 100644 src/viewer/ViewerToolbar.vue delete mode 100644 templates/editor.php 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'), }, {