storage->delete('servers.json'); $this->game->start(MainForm::class); /** @var MainForm $form */ $form = $this->game->scene(); $form->session->reset(); $this->game->start(MainForm::class, ['connectDemo' => true]); $this->form = $form; $this->game->run(10); $c = $form->session->connections[0]; self::assertTrue($c->isConnected(), 'the demo pool connects: ' . $c->error); $this->until(fn () => $form->tree->node((string)$c->cache->master()) !== null, 60); $form->tree->select($this->vm($c, 'db-primary')); $form->tabs->selectTab('Console'); $this->game->run(2); $rfb = $form->console(); self::assertNotNull($rfb); $this->until(fn () => $rfb->state === RfbClient::READY, 120); $this->until(fn () => str_ends_with($this->lines($rfb)[count($this->lines($rfb)) - 1] ?? '', 'login: '), 120); return $rfb; } private function vm(Connection $c, string $name): string { foreach ($c->cache->all('vm') as $ref => $vm) { if ($vm['name_label'] === $name) { return $ref; } } self::fail("no VM $name"); } private function until(callable $condition, int $frames = 600): void { for ($i = 0; $i < $frames && !$condition(); $i++) { $this->game->run(1); } self::assertTrue((bool)$condition(), 'still waiting after ' . $frames . ' frames'); } /** @return list the text of the demo VM's console */ private function lines(RfbClient $rfb): array { $stream = (fn () => $this->stream)->call($rfb); self::assertInstanceOf(FakeVnc::class, $stream); return (fn () => $this->lines)->call($stream); } /** A key typed on the real keyboard: SDL's key down, its text event only while the system's text input is on (as SDL does). */ private function typeKey(int $sdlKey, string $text = ''): void { $keyboard = Engine::get()->input->getKeyboard(); $keyboard->down($sdlKey); if ($text !== '' && \SDL_IsTextInputActive()) { $keyboard->addText($text); } $this->game->run(1); $keyboard->up($sdlKey); $this->game->run(1); } public function testLettersTypedOnTheKeyboardReachTheVmAfterAFieldWasTypedInto(): void { $rfb = $this->openConsole(); // the search box typed into first: text events came once, the form now takes the characters from them only $this->form->click($this->form->search); $this->game->run(1); $this->typeKey(ord('x'), 'x'); $this->typeKey(Pointer::BACKSPACE); self::assertTrue(Engine::get()->input->getKeyboard()->hasTextEvents()); // a click gives the keyboard to the console (the form turns the text input off for a widget that is not a TextBox) $this->form->click($this->form->vnc); $this->game->run(1); self::assertSame($this->form->vnc, $this->form->focused()); self::assertTrue((bool)\SDL_IsTextInputActive(), 'the console keeps the system text input on'); foreach (str_split('root') as $char) { $this->typeKey(ord($char), $char); } $this->typeKey(Pointer::RETURN); $this->until(fn () => in_array('Password: ', $this->lines($rfb), true), 60); self::assertContains('db-primary login: root', $this->lines($rfb), 'the letters reached the VM'); } public function testTheConsoleGoesFullscreenAndComesBackWithTheKeyboardStillOnTheVm(): void { $rfb = $this->openConsole(); $form = $this->form; self::assertSame('Fullscreen', $form->consoleFull->text); $before = [$form->vnc->w, $form->vnc->h]; $form->vnc->scale = false; $form->click($form->consoleFull); $this->game->run(2); self::assertTrue($form->isConsoleFullscreen()); self::assertFalse($form->left->visible, 'the tree is hidden'); self::assertFalse($form->menu->visible, 'the menu bar is hidden'); self::assertFalse($form->tabs->visible, 'the tabs are hidden'); self::assertTrue($form->vnc->scale, 'scaled to fit'); self::assertSame('Exit Fullscreen', $form->consoleFull->text); self::assertSame($form->vnc, $form->focused(), 'the keyboard goes to the VM'); self::assertGreaterThan($before[0] + 200, $form->vnc->w, 'the screen takes the window'); self::assertGreaterThan($before[1] + 50, $form->vnc->h); // Escape still goes to the VM, it does not leave $form->key(Pointer::ESCAPE); $this->game->run(2); self::assertTrue($form->isConsoleFullscreen(), 'Escape is the VM\'s'); $form->type('root'); $form->key(Pointer::RETURN); $this->until(fn () => in_array('Password: ', $this->lines($rfb), true), 60); $this->game->shot('console-fullscreen'); // Ctrl+Alt+F leaves (and is not sent to the VM) $form->vnc->alt = true; $p = new Pointer(); $p->keys[] = ord('f'); $p->ctrl = true; self::assertTrue($form->vnc->keys($p)); $form->vnc->alt = false; $this->game->run(2); self::assertFalse($form->isConsoleFullscreen()); self::assertTrue($form->left->visible && $form->menu->visible && $form->tabs->visible, 'the window is back'); self::assertFalse($form->vnc->scale, 'the scale chosen before'); self::assertSame('Fullscreen', $form->consoleFull->text); self::assertEqualsWithDelta($before[0], $form->vnc->w, 1); // the button too; another tab ends the fullscreen $form->click($form->consoleFull); $this->game->run(2); self::assertTrue($form->isConsoleFullscreen()); $form->click($form->consoleFull); $this->game->run(2); self::assertFalse($form->isConsoleFullscreen()); $form->click($form->consoleFull); $this->game->run(2); $form->tabs->selectTab('General'); $this->game->run(2); self::assertFalse($form->isConsoleFullscreen()); self::assertTrue($form->left->visible, 'the window is back when the console closes'); } protected function tearDown(): void { FakeVnc::$extendedKeys = false; FakeVnc::$layout = 'us'; parent::tearDown(); } /** Physical keys pressed and let go one after the other (SDL scancodes), as SDL_GetKeyboardState() shows them frame by frame. */ private function pressPhysical(array $scancodes, array $alsoHeld = []): void { $state = []; $this->form->vnc->keyState = function () use (&$state): array { return $state; }; foreach ($scancodes as $scancode) { $state = array_fill_keys([...$alsoHeld, $scancode], 1); $this->game->run(2); $state = array_fill_keys($alsoHeld, 1); $this->game->run(2); } $state = []; $this->game->run(2); } public function testAnAzertyVmTypesWhatTheAzertyKeyboardSaysWithPhysicalKeys(): void { FakeVnc::$extendedKeys = true; // QEMU's console: it takes physical keys FakeVnc::$layout = 'fr'; // the VM is set to AZERTY $rfb = $this->openConsole(); $this->until(fn () => $rfb->extendedKeys, 60); $this->form->click($this->form->vnc); self::assertSame($this->form->vnc, $this->form->focused()); // the keys where AZERTY has a, z, e, r, t, y, then Shift + the key of q: what the user's keyboard shows $this->pressPhysical([20, 26, 8, 21, 23, 28]); $this->pressPhysical([4], [225]); $lines = $this->lines($rfb); self::assertStringEndsWith('login: azertyQ', $lines[count($lines) - 1]); $this->game->shot('console-azerty'); } public function testQuickTapsBetweenTwoFramesStillReachTheVmAsPhysicalKeys(): void { FakeVnc::$extendedKeys = true; FakeVnc::$layout = 'fr'; $rfb = $this->openConsole(); $this->until(fn () => $rfb->extendedKeys, 60); $this->form->click($this->form->vnc); $taps = []; $this->form->vnc->keyState = fn (): array => []; // never seen held: down and up in the same frame $this->form->vnc->keyTaps = function () use (&$taps): array { return $taps; }; foreach ([[20], [26], [225, 4]] as $frame) { // a, z, then Shift + the key of q, AZERTY $taps = $frame; $this->game->run(1); $taps = []; $this->game->run(1); } $lines = $this->lines($rfb); self::assertStringEndsWith('login: azQ', $lines[count($lines) - 1]); } public function testWithoutPhysicalKeysQemuReadsCharactersAsAUsKeyboard(): void { // why the physical keys: a server without the extension gets characters, which QEMU types on a US keyboard FakeVnc::$layout = 'fr'; $rfb = $this->openConsole(); $this->game->run(5); self::assertFalse($rfb->extendedKeys); $rfb->tap(ord('a')); $this->game->run(5); $lines = $this->lines($rfb); self::assertStringEndsWith('login: q', $lines[count($lines) - 1], 'the AZERTY VM reads the US "a" key as q'); } public function testThePictureGrowsByAWholeFactorSoTheLettersStaySharp(): void { $rfb = $this->openConsole(); $vnc = $this->form->vnc; foreach ([[800, 500, 1.0], [1300, 820, 2.0], [700, 300, 0.75]] as [$w, $h, $k]) { $vnc->w = $w; $vnc->h = $h; self::assertSame($k, $vnc->picture()[4], "{$w}x{$h}"); } } }