#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import time
import os

import parent
from testlib import *
from machine_core.constants import TEST_OS_DEFAULT


@nondestructive
@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg", "rhel-8-4-distropkg")
class TestPages(MachineCase):
    def checkDocs(self, items):
        m = self.machine
        b = self.browser

        b.click("#navbar-docs > a")
        b.wait_visible("#navbar-docs-items")
        expected = "Web Console"
        expected += "".join(items)
        expected += "About Web Console"
        # DOCUMENTATION_URL is only in Fedora and RHEL >= 8.4
        if "fedora" in m.image:
            expected = "Fedora documentation" + expected
        elif m.image.startswith("rhel-8-4"):
            expected = "Red Hat Enterprise Linux documentation" + expected
        b.wait_collected_text("#navbar-docs-items", expected)
        b.click("#navbar-docs > a")
        b.wait_not_visible("#navbar-docs-items")

    def open_lang_modal(self):
        self.browser.switch_to_top()
        self.browser.click("#navbar-dropdown")

        self.browser.click(".display-language-menu a")
        self.browser.wait_visible('#display-language-modal')


    def testBasic(self):
        m = self.machine
        b = self.browser
        self.restore_dir("/etc/systemd/system", post_restore_action="systemctl stop test.timer test.service; systemctl daemon-reload")
        m.write("/etc/systemd/system/test.service",
                """
[Unit]
Description=Test Service

[Service]
ExecStart=/bin/true

[Install]
WantedBy=default.target
""")
        m.write("/etc/systemd/system/test.timer",
                """
[Unit]
Description=Test timer

[Timer]
OnCalendar=daily
""")
        # After writing files out tell systemd about them
        m.execute("systemctl daemon-reload")

        m.execute("systemctl start test.timer")

        self.allow_restart_journal_messages()
        self.allow_journal_messages("Failed to get realtime timestamp: Cannot assign requested address")

        # On Debian and Ubuntu we have to generate the other locales
        if "debian" in m.image:
            m.execute("echo 'de_DE.UTF-8 UTF-8' >> /etc/locale.gen && locale-gen && update-locale")
        elif "ubuntu" in m.image:
            m.execute("locale-gen de_DE && locale-gen de_DE.UTF-8 && update-locale")

        # login so that we have a cookie.
        self.login_and_go("/system/services#/test.service")

        # check that reloading a page with parameters works
        b.enter_page("/system/services")
        b.reload()
        b.enter_page("/system/services")
        b.wait_text(".service-name", "Test Service")
        b.switch_to_top()
        self.checkDocs(["Managing services"])
        b.click("#navbar-docs > a")
        b.wait_visible('#navbar-docs-items a:contains("Managing services")[href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-services-in-the-web-console_system-management-using-the-rhel-8-web-console"]')
        b.wait_visible('#navbar-docs-items a:contains("Web Console")[href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/index"]')
        b.click("#navbar-docs > a")
        b.wait_not_visible("#navbar-docs-items")
        b.go("/network")
        self.checkDocs(["Managing networking bonds", "Managing networking teams", "Managing networking bridges", "Managing VLANs", "Managing firewall"])
        b.go("/system/services")

        m.restart_cockpit()
        b.relogin("/system/services")
        b.wait_text(".service-name", "Test Service")

        # check that navigating away and back preserves place
        b.switch_to_top()
        b.click("a[href='/system']")
        b.enter_page("/system")
        b.wait_visible("#system_information_systime_button")
        b.switch_to_top()
        self.checkDocs(["Configuring system settings"])
        b.click("a[href='/system/services']")
        b.enter_page("/system/services")
        b.wait_visible("ol.pf-c-breadcrumb__list")
        b.wait_text(".service-name", "Test Service")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname === "/system/services"')
        b.wait_js_cond('window.location.hash === "#/test.service"')

        # check that when inside the component clicking the navbar
        # takes you home
        b.switch_to_top()
        b.click("a[href='/system/services']")
        b.enter_page("/system/services")
        b.wait_visible("#services-list")
        b.wait_not_present("#service-details")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname === "/system/services"')
        b.wait_js_cond('window.location.hash === ""')

        # Navigate inside an iframe
        b.switch_to_top()
        b.go("/@localhost/playground/test")
        b.enter_page("/playground/test")
        b.click("button:contains('Go down')")
        b.click("button:contains('Go down')")
        b.switch_to_top()
        b.wait_js_cond("window.location.hash == '#/0/1?length=1'")

        # This should be visible now
        b.switch_to_frame("cockpit1:localhost/playground/test")
        b.wait_visible("#hidden")

        # This should now say invisible
        b.switch_to_top()
        b.go("/@localhost/system/services")
        b.switch_to_frame("cockpit1:localhost/playground/test")
        b.wait_not_visible("#hidden")

        b.switch_to_top()

        b.wait_visible("#nav-system")

        # Lets try changing the language

        self.open_lang_modal()
        b.set_val("#display-language-modal select", "de-de")
        b.click("#display-language-modal footer button.pf-m-primary")
        b.expect_load()
        if b.cdp.browser in ["chrome", "chromium"]:
            # HACK: work around language switching in Chrome not working in current session (issue #8160)
            b.reload(ignore_cache=True)
        b.wait_visible("#content")

        # Check that the system page is translated
        b.go("/system")
        b.enter_page("/system")
        b.wait_in_text(".ct-overview-header", "Neustart")

        # Systemd timer localization
        b.go("/system/services")
        b.switch_to_top()
        b.wait_js_cond('document.title.indexOf("Dienste") === 0')
        b.enter_page("/system/services")
        b.click('#services-filter li:nth-child(4) a')
        # HACK: the timers' next run/last trigger (col 3/4) don't always get filled (issue #9439)
        # b.wait_in_text("tr[data-goto-unit='test\.timer'] td:nth-child(3)", "morgen um")

        # BIOS date parsing using moment
        b.go("/system/hwinfo")
        b.enter_page("/system/hwinfo")
        b.wait_in_text("#hwinfo .info-table-ct tbody:nth-of-type(2) tr:nth-of-type(3) td", ".20")

        # Check the playground page
        b.switch_to_top()
        b.go("/playground/translate")
        b.wait_js_cond('document.title.indexOf("Entwicklung") === 0')
        b.enter_page("/playground/translate")

        # HTML section
        self.assertEqual(b.text("#translate-html"), "Bereit")
        self.assertEqual(b.text("#translate-html-context"), "Bereiten")
        self.assertEqual(b.text("#translate-html-yes"), "Nicht bereit")
        self.assertEqual(b.attr("#translate-html-title", "title"), u"Nicht verfügbar")
        self.assertEqual(b.text("#translate-html-title"), "Cancel")
        self.assertEqual(b.attr("#translate-html-yes-title", "title"), u"Nicht verfügbar")
        self.assertEqual(b.text("#translate-html-yes-title"), "Abbrechen")

        # Glade section
        self.assertEqual(b.text("#translatable-glade"), "Leer")
        self.assertEqual(b.text("#translatable-glade-context"), "Leeren")

        # Mustache section
        self.assertEqual(b.text("#translatable-mustache"), "Benutzer")
        self.assertEqual(b.text("#translate-mustache"), "Speicher")
        self.assertEqual(b.text("#translate-mustache-yes"), "Konten")
        self.assertEqual(b.attr("#translate-mustache-title", "title"), "Fehler")
        self.assertEqual(b.text("#translate-mustache-title"), "User")
        self.assertEqual(b.attr("#translate-mustache-yes-title", "title"), "Fehler")
        self.assertEqual(b.text("#translate-mustache-yes-title"), "Benutzer")

        # Javascript
        self.assertEqual(b.text("#underscore-empty"), "Leer")
        self.assertEqual(b.text("#underscore-context-empty"), "Leeren")
        self.assertEqual(b.text("#cunderscore-context-empty"), "Leeren")
        self.assertEqual(b.text("#gettext-control"), "Steuerung")
        self.assertEqual(b.text("#gettext-context-control"), "Strg")
        self.assertEqual(b.text("#ngettext-disks-1"), "$0 Festplatte fehlt")
        self.assertEqual(b.text("#ngettext-disks-2"), "$0 Festplatten fehlen")
        self.assertEqual(b.text("#ngettext-context-disks-1"), u"$0 Datenträger fehlt")
        self.assertEqual(b.text("#ngettext-context-disks-2"), u"$0 Datenträger fehlen")

        # Frame title
        b.switch_to_top()
        b.wait_attr("iframe[name='cockpit1:localhost/system']", "title", "Überblick")

        # Log out and check that login page is translated now
        b.logout()
        b.wait_visible('#password-group')
        b.wait_text("#password-group > label", "Passwort")

        # Test all languages
        # Test that pages do not oops and that locale is valid

        if m.image not in [TEST_OS_DEFAULT] and not m.image.startswith("rhel-"):
            return

        def line_sel(i):
            return '.terminal .xterm-accessibility-tree div:nth-child(%d)' % i

        pages = ["/system", "/system/logs", "/network", "/users", "/system/services", "/system/terminal"]

        self.login_and_go('/system')
        b.wait_visible('#overview')

        self.open_lang_modal()
        languages = b.eval_js("ph_select('#display-language-list option').map(function(e) { return e.value })")
        b.click("#display-language-modal footer button.pf-m-link") # Close the menu

        for language in languages:
            b.go("/system")
            b.enter_page("/system")

            self.open_lang_modal()
            b.set_val("#display-language-modal select", language)
            b.click("#display-language-modal footer button.pf-m-primary")
            b.expect_load()
            if b.cdp.browser in ["chrome", "chromium"]:
                # HACK: work around language switching in Chrome not working in current session (issue #8160)
                b.reload(ignore_cache=True)
            b.wait_visible("#content")

            # Test some pages, end up in terminal
            for page in pages:
                b.go(page)
                b.enter_page(page)

            locale = language.split("-")
            if len(locale) == 1:
                locale.append("")
            locale = "{0}_{1}.UTF-8".format(locale[0], locale[1].upper())

            b.wait_visible(".terminal .xterm-accessibility-tree")
            b.wait_in_text(line_sel(1), "admin")
            b.key_press("echo $LANG\r")
            b.wait_in_text(line_sel(2), locale)

            b.switch_to_top()

            b.wait_js_func("""(function (lang) {
                let correct = true;
                document.querySelectorAll('#content iframe').forEach(el => {
                    if (el.contentDocument.documentElement.lang !== lang)
                        correct = false;
                });
                return correct;
            })""", language)
            b.wait_attr(".index-page", "lang", language)

    def testPtBRLocale(self):
        m = self.machine
        b = self.browser

        m.execute('useradd scruffy -s /bin/bash -c \'Scruffy\' || true')
        m.execute('echo scruffy:foobar | chpasswd')

        self.allow_restart_journal_messages()

        if "debian" in m.image:
            m.execute('echo \'pt_BR.UTF-8 UTF-8\' >> /etc/locale.gen && locale-gen && update-locale')
        elif "ubuntu" in m.image:
            m.execute('locale-gen pt_BR && locale-gen pt_BR.UTF-8 && update-locale')

        self.login_and_go('/system')
        b.wait_visible('#overview')
        self.open_lang_modal()
        b.set_val('#display-language-modal select', 'pt-br')
        b.click('#display-language-modal footer button.pf-m-primary')
        b.expect_load()
        if b.cdp.browser in ["chrome", "chromium"]:
            # HACK: work around language switching in Chrome not working in current session (issue #8160)
            b.reload(ignore_cache=True)
        b.wait_visible('#content')

        # Check that the system page is translated
        b.go('/system')
        b.enter_page('/system')
        b.wait_in_text('.ct-overview-header', 'Reiniciar')

        # Systemd timer localization
        b.go('/system/services')
        b.enter_page('/system/services')
        b.click('#services-filter li:nth-child(4) a')
        # HACK: the timers' next run/last trigger (col 3/4) don't always get filled (issue #9439)
        # b.wait_in_text('tr[data-goto-unit=\'test\.timer\'] td:nth-child(3)', 'morgen um')

        # Check the playground page
        b.switch_to_top()
        b.go('/playground/translate')
        b.enter_page('/playground/translate')

        # HTML section
        self.assertEqual(b.text('#translate-html'), 'Pronto')
        self.assertEqual(b.text('#translate-html-context'), 'Pronto')
        self.assertEqual(b.text('#translate-html-yes'), u'Não está pronto')
        self.assertEqual(b.attr('#translate-html-title', 'title'), u'Indisponível')
        self.assertEqual(b.text('#translate-html-title'), 'Cancel')
        self.assertEqual(b.attr('#translate-html-yes-title', 'title'), u'Indisponível')
        self.assertEqual(b.text('#translate-html-yes-title'), 'Cancelar')

        # Glade section
        self.assertEqual(b.text('#translatable-glade'), 'Vazio')
        self.assertEqual(b.text('#translatable-glade-context'), 'Vazio')

        # Mustache section
        self.assertEqual(b.text('#translatable-mustache'), u'Usuário')
        self.assertEqual(b.text('#translate-mustache'), u'Memória')
        self.assertEqual(b.text('#translate-mustache-yes'), 'Contas')
        self.assertEqual(b.attr('#translate-mustache-title', 'title'), 'Erro')
        self.assertEqual(b.text('#translate-mustache-title'), 'User')
        self.assertEqual(b.attr('#translate-mustache-yes-title', 'title'), 'Erro')
        self.assertEqual(b.text('#translate-mustache-yes-title'), u'Usuário')

        # Javascript
        self.assertEqual(b.text('#underscore-empty'), 'Vazio')
        self.assertEqual(b.text('#underscore-context-empty'), 'Vazio')
        self.assertEqual(b.text('#cunderscore-context-empty'), 'Vazio')
        self.assertEqual(b.text('#gettext-control'), 'Controle')
        self.assertEqual(b.text('#gettext-context-control'), 'Controle')
        self.assertEqual(b.text('#ngettext-disks-1'), u'$0 disco não encontrado')
        self.assertEqual(b.text('#ngettext-disks-2'), u'$0 discos não encontrados')
        self.assertEqual(b.text('#ngettext-context-disks-1'), u'$0 disco não encontrado')
        self.assertEqual(b.text('#ngettext-context-disks-2'), u'$0 discos não encontrados')

        # Log out and check that login page is translated now
        b.logout()
        b.wait_text('#password-group > label', 'Senha')

    def testFrameReload(self):
        b = self.browser
        frame = "cockpit1:localhost/playground/test"
        self.addCleanup(self.machine.execute, "rm -f /tmp/counter")

        self.login_and_go("/playground/test")

        b.wait_text('#file-content', "0")
        b.click("#modify-file")
        b.wait_text('#file-content', "1")

        b.switch_to_top()
        b.eval_js('ph_set_attr("iframe[name=\'%s\']", "data-ready", null)' % frame)
        b.eval_js('ph_set_attr("iframe[name=\'%s\']", "src", "../playground/test.html?i=1#/")' % frame)
        b.expect_load_frame("cockpit1:localhost/playground/test")
        b.wait_visible("iframe.container-frame[name='%s'][data-ready]" % frame)

        b.enter_page("/playground/test")

        b.wait_text('#file-content', "1")

        self.allow_restart_journal_messages()

    def testShellReload(self):
        b = self.browser
        m = self.machine

        self.login_and_go()

        b.wait_in_text("#host-apps", "Overview")
        self.restore_dir("/home/admin")
        m.execute("mkdir -p /home/admin/.local/share/cockpit/foo")
        m.write("/home/admin/.local/share/cockpit/foo/manifest.json",
                '{ "menu": { "index": { "label": "FOO!" } } }')
        b.reload()
        b.wait_in_text("#host-apps", "FOO!")

        self.allow_restart_journal_messages()

    def testMenuSearch(self):
        b = self.browser
        m = self.machine

        # On Ubuntu and Debian we would need to generate locales - just ignore it
        self.allow_journal_messages("invalid or unusable locale: de_DE.UTF-8")

        self.login_and_go()

        filter_sel = ".filter-menus"

        # Check that some page disappears and some stay
        b.focus(filter_sel)
        b.key_press("se")
        b.wait_not_present("#host-apps li a:contains('Logs')")
        b.wait_visible("#host-apps li a:contains('Services')")
        b.wait_text("#host-apps li a:contains('Services') mark", "Se")

        b.focus(filter_sel)
        b.key_press("\b\b")
        b.wait_visible("#host-apps li a:contains('Logs')")
        b.wait_visible("#host-apps li a:contains('Services')")

        # Check that any substring work
        b.focus(filter_sel)
        b.key_press("CoUN")
        b.wait_not_present("#host-apps li a:contains('Overview')")
        b.wait_visible("#host-apps li a:contains('Accounts')")
        b.wait_text("#host-apps li a:contains('Accounts') mark", "coun")

        # Check it can also search by keywords
        b.focus(filter_sel)
        b.key_press("\b\b\b\bsystemd")
        b.wait_visible("#host-apps li a:contains('Services')")
        b.wait_text("#host-apps li a:contains('Services')", "ServicesContains: systemd")
        b.wait_text("#host-apps li a:contains('Services') mark", "systemd")

        # Check that enter activates first result
        b.focus(filter_sel)
        b.key_press("\b\b\b\b\b\b\blogs")
        b.wait_not_present("#host-apps li a:contains('Services')")
        b.wait_visible("#host-apps li a:contains('Logs')")
        b.focus(filter_sel)
        b.key_press("\r")
        b.enter_page("/system/logs")
        b.wait_visible("#journal")

        # Visited page, search should be cleaned up
        b.switch_to_top()
        b.wait_val(filter_sel, "")

        # Check that escape cleans the search
        b.key_press("logs")
        b.wait_not_present("#host-apps li a:contains('Services')")
        b.wait_visible("#host-apps li a:contains('Logs')")
        b.focus(filter_sel)
        b.key_press(chr(27)) # escape
        b.wait_val(filter_sel, "")
        b.wait_visible("#host-apps li a:contains('Services')")

        # Check that clicking on `Clear search` cleans the search
        b.key_press("logs")
        b.wait_not_present("#host-apps li a:contains('Services')")
        b.wait_visible("#host-apps li a:contains('Logs')")
        b.click("button:contains('Clear search')")
        b.key_press("\b\b\b\b")
        b.wait_visible("#host-apps li a:contains('Services')")
        b.wait_not_present("button:contains('Clear search')")

        # Check that arrows navigate the menu
        b.focus(filter_sel)
        b.key_press("s")
        b.wait_not_present("#host-apps li a:contains('Logs')")
        b.key_press(chr(40), use_ord=True) # arrow down
        b.key_press(chr(40), use_ord=True) # arrow down
        b.key_press("\r")
        if m.image in ["fedora-coreos"]:
            b.enter_page("/users")
        else:
            b.enter_page("/storage")

        # Check we jump into subpage when defined in manifest
        b.switch_to_top()
        b.focus(filter_sel)
        b.key_press("firew")
        b.wait_visible("#host-apps li a:contains('Networking')")
        b.wait_not_present("#host-apps li a:contains('Overview')")
        b.click("#host-apps li a:contains('Networking')")
        b.enter_page("/network/firewall")

        # Search internationalized menu
        self.open_lang_modal()
        b.set_val("#display-language-modal select", "de-de")
        b.click("#display-language-modal footer button.pf-m-primary")
        b.expect_load()
        if b.cdp.browser in ["chrome", "chromium"]:
            # HACK: work around language switching in Chrome not working in current session (issue #8160)
            b.reload(ignore_cache=True)
        b.wait_visible("#content")
        b.go("/system")
        b.enter_page("/system")
        b.wait_in_text(".ct-overview-header", "Neustart")

        b.switch_to_top()
        b.wait_visible("#host-apps li a:contains('Dienste')")
        b.wait_visible("#host-apps li a:contains('Protokolle')")
        b.focus(filter_sel)
        b.key_press("dien")
        b.wait_not_present("#host-apps li a:contains('Protokolle')")
        b.wait_visible("#host-apps li a:contains('Dienste')")
        b.wait_text("#host-apps li a:contains('Dienste') mark", "Dien")

    def testShellPreload(self):
        b = self.browser
        m = self.machine

        self.login_and_go()

        # Check what's going on while playground/preloaded is still invisible
        b.switch_to_top()
        b.wait_attr('iframe[name="cockpit1:localhost/playground/preloaded"]', 'data-loaded', 1)
        b.switch_to_frame("cockpit1:localhost/playground/preloaded")
        b.wait_js_func('ph_text_is', "#host", m.execute("hostname").replace("\n", ""))
        time.sleep(3)
        b.wait_js_func('ph_text_is', "#release", "")

        # Now navigate to it.
        b.switch_to_top()
        b.go("/playground/preloaded")
        b.enter_page("/playground/preloaded")
        b.wait_text("#release", m.execute("cat /etc/os-release").replace("\n", ""))

    def testReactPatterns(self):
        b = self.browser
        m = self.machine

        stuff = os.path.join(self.vm_tmpdir, "stuff")
        # prepare a directory for testing file autocomplete widget
        m.execute("mkdir -p {0}/dir1; touch {0}/file1.txt".format(stuff))

        self.login_and_go("/playground/react-patterns")

        # test file completion widget
        b.focus("#demo-file-ac input[type=text]")
        b.key_press(stuff + "/")
        # need to wait for the widget's "fast typing" inhibition delay to trigger the completion popup
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/")
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(2) button", "dir1/")
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(3) button", "file1.txt")
        b.click("#file-autocomplete-widget li:nth-of-type(2) button")

        # clear the file completion widget
        b.click("#demo-file-ac div:first-of-type div:first-of-type button:nth-of-type(1)")
        b.wait_not_present("#file-autocomplete-widget li")
        b.focus("#demo-file-ac input[type=text]")
        b.key_press(stuff + "/")
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/")
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(3) button", "file1.txt")
        b.click("#file-autocomplete-widget li:nth-of-type(3) button")
        b.wait_not_present("#file-autocomplete-widget li")

        # now update file1, check robustness with dynamic events
        m.execute("touch %s/file1.txt" % stuff)
        b.focus("#demo-file-ac input[type=text]")
        time.sleep(1)
        b.key_press(["\b"]*5)
        # input is now $stuff/file
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", "file1.txt")
        b.key_press(["\b"]*4)
        # input is now $stuff/, so all listings should be available
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(3) button", "file1.txt")

        # add new file
        m.execute("touch %s/other" % stuff)
        b.focus("#demo-file-ac input[type=text]")
        # We need to tickle the widget to re-read the directory by changing to
        # the previous directory and back to the directory we want to list.
        # This is an implementation choice, to avoid re-reading the directories
        # content with every user input change, which is definitely a performance cost
        b.key_press(["\b"]*6)
        time.sleep(1)
        b.key_press("stuff/")
        b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "other")

    @skipImage("No PCP available", "fedora-coreos")
    def testPlots(self):
        b = self.browser
        m = self.machine

        self.addCleanup(m.execute, "systemctl stop pmcd")
        m.execute("systemctl start pmcd")

        self.login_and_go("/playground/plot")
        b.wait_visible("#plot-direct")
        b.wait_visible("#plot-pmcd")

        def read_mem_info(machine):
            info = {}
            for l in machine.execute("cat /proc/meminfo").splitlines():
                (name, value) = l.strip().split(":")
                if value.endswith("kB"):
                    info[name] = int(value[:-2]) * 1024
                else:
                    info[name] = int(value)
            return info

        # When checking whether the plots show the expected results,
        # we look for a segment of the data of a certain duration
        # whose average is in a certain range.  Otherwise any short
        # outlier will make us miss the expected plateau.  Such
        # outliers happen frequently with the CPU plot.  We also
        # insist that the first and last value of the segment are in
        # range, otherwise we would find any arbitrary average in a
        # graph with a slope.

        b.inject_js("""
          ph_plateau = function (data, min, max, duration, label) {
              var i, j;
              var sum;  // sum of data[i..j]

              function ok(val) {
                  return val >= min && val <= max;
              }

              sum = 0;
              i = 0;
              for (j = 0; j < data.length; j++) {
                  sum += data[j][1];
                  while (i < j && (data[j][0] - data[i][0]) > duration * 1000) {
                      avg = sum / (j - i + 1);
                      if (ok(avg) && ok(data[i][1]) && ok(data[j][1]))
                          return true;
                      sum -= data[i][1];
                      i++;
                  }
              }
            return false;
          }
        """)

        b.inject_js("""
          ph_plot_data_plateau = function (sel, min, max, duration, label) {
            return ph_plateau(window.plot_state.data(sel)[0].data, min, max, duration, label);
          }
        """)

        meminfo = read_mem_info(m)
        mem_avail = meminfo['MemAvailable']
        b.wait_js_func("ph_plot_data_plateau", "direct", mem_avail * 0.95, mem_avail * 1.05, 15, "mem")

        meminfo = read_mem_info(m)
        mem_avail = meminfo['MemAvailable']
        b.wait_js_func("ph_plot_data_plateau", "pmcd", mem_avail * 0.95, mem_avail * 1.05, 15, "mem")

    def testPageStatus(self):
        b = self.browser

        self.login_and_go("/playground")

        b.set_input_text("#type", "info")
        b.set_input_text("#title", "My Little Page Status")
        b.click("#set-status")

        b.switch_to_top()
        b.wait_visible("#development-info")
        b.mouse("#development-info", "mouseenter")
        b.wait_in_text(".pf-c-tooltip", "My Little Page Status")
        b.mouse("#development-info", "mouseleave")

        b.go("/playground/notifications-receiver")
        b.enter_page("/playground/notifications-receiver")
        b.wait_text("#received-type", "info")
        b.wait_text("#received-title", "My Little Page Status")

        b.switch_to_top()
        b.go("/playground")
        b.enter_page("/playground")
        b.click("#clear-status")

        b.switch_to_top()
        b.wait_not_present("#development-info")

        b.go("/playground/notifications-receiver")
        b.enter_page("/playground/notifications-receiver")
        b.wait_text("#received-type", "-")
        b.wait_text("#received-title", "-")

if __name__ == '__main__':
    test_main()
