TR
games/xencenter-xenadmin-xcp-jumphp / tests / ConsoleTest.php · commit fb1b7c9
<?phpnamespace Tests;use App\Form\MainForm;use App\Model\Connection;use App\Vnc\FakeVnc;use App\Vnc\RfbClient;use JumPHP\Gui\Pointer;use JumPHP\Kernel\Engine;use JumPHP\Testing\GameTestCase;/** The VNC console: the real keyboard reaching the VM (as in a browser), the fullscreen console. */final class ConsoleTest extends GameTestCase{    private MainForm $form;    /** The demo pool, the console of db-primary open and showing its login prompt. */    private function openConsole(): RfbClient    {        Engine::get()->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<string> 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}");        }    }}