Add more unit and integration tests (#773)

* Add unit tests for pyloader and pytitle

* Add more unit tests for pyrepl, pybox and pyinputbox

* Add more tests for pyscript and pyconfig

* White space

* Fix d3 tests and improve more examples test

* Update matplotlib test

* Add numpy to dependencies

* Address Madhur comments

* Update test name
This commit is contained in:
Fábio Rosado
2022-09-26 23:17:32 +01:00
committed by GitHub
parent d033ab04da
commit b674515d06
12 changed files with 482 additions and 53 deletions

View File

@@ -11,6 +11,8 @@ dependencies:
- isort
- codespell
- pre-commit
- pillow
- numpy
- pip:
- playwright

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,8 +1,13 @@
import base64
import io
import math
import os
import re
import time
import numpy as np
import pytest
from PIL import Image
from .support import ROOT, PyScriptTest
@@ -80,7 +85,6 @@ class TestExamples(PyScriptTest):
wait_for_render(self.page, "*", '<canvas.*?class=\\"marks\\".*?>')
save_as_png_link = self.page.locator("text=Save as PNG")
see_source_link = self.page.locator("text=View Source")
# These shouldn't be visible since we didn't click the menu
assert not save_as_png_link.is_visible()
assert not see_source_link.is_visible()
@@ -105,32 +109,66 @@ class TestExamples(PyScriptTest):
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
@pytest.mark.xfail(reason="Flaky test #759")
def test_d3(self):
# XXX improve this test
self.goto("examples/d3.html")
self.wait_for_pyscript()
assert (
self.page.title() == "d3: JavaScript & PyScript visualizations side-by-side"
)
wait_for_render(self.page, "*", "<svg.*?>")
assert "PyScript version" in self.page.content()
pyscript_chart = self.page.wait_for_selector("#py")
# Let's simply assert that the text of the chart is as expected which
# means that the chart rendered successfully and with the right text
assert "🍊21\n🍇13\n🍏8\n🍌5\n🍐3\n🍋2\n🍎1\n🍉1" in pyscript_chart.inner_text()
def test_folium(self):
# XXX improve this test
self.goto("examples/folium.html")
self.wait_for_pyscript()
assert self.page.title() == "Folium"
wait_for_render(self.page, "*", "<iframe srcdoc=")
# We need to look into the iframe first
iframe = self.page.frame_locator("iframe")
# Just checking that legend was rendered correctly
legend = iframe.locator("#legend")
assert "Unemployment Rate (%)" in legend.inner_html()
# Let's check that the zoom buttons are rendered and clickable
# Note: if element is not clickable it will timeout
zoom_in = iframe.locator("[aria-label='Zoom in']")
assert "+" in zoom_in.inner_text()
zoom_in.click()
zoom_out = iframe.locator("[aria-label='Zoom out']")
assert "" in zoom_out.inner_text()
zoom_out.click()
def test_matplotlib(self):
# XXX improve this test
self.goto("examples/matplotlib.html")
self.wait_for_pyscript()
assert self.page.title() == "Matplotlib"
wait_for_render(self.page, "*", "<img src=['\"]data:image")
# The image is being rended using base64, lets fetch its source
# and replace everything but the actual base64 string.\
img_src = (
self.page.wait_for_selector("img")
.get_attribute("src")
.replace("data:image/png;charset=utf-8;base64,", "")
)
# Finally, let's get the np array from the previous data
img_data = np.asarray(Image.open(io.BytesIO(base64.b64decode(img_src))))
with Image.open(
os.path.join(os.path.dirname(__file__), "test_assets", "tripcolor.png"),
) as image:
ref_data = np.asarray(image)
# Now that we have both images data as a numpy array
# let's confirm that they are the same
deviation = np.mean(np.abs(img_data - ref_data))
assert deviation == 0.0
def test_numpy_canvas_fractals(self):
# XXX improve this test
self.goto("examples/numpy_canvas_fractals.html")
self.wait_for_pyscript()
assert (
@@ -141,12 +179,57 @@ class TestExamples(PyScriptTest):
self.page, "*", "<div.*?id=['\"](mandelbrot|julia|newton)['\"].*?>"
)
# Assert that we get the title and canvas for each element
mandelbrot = self.page.wait_for_selector("#mandelbrot")
assert "Mandelbrot set" in mandelbrot.inner_text()
assert "<canvas" in mandelbrot.inner_html()
julia = self.page.wait_for_selector("#julia")
assert "Julia set" in julia.inner_text()
assert "<canvas" in julia.inner_html()
newton = self.page.wait_for_selector("#newton")
assert "Newton set" in newton.inner_text()
assert "<canvas" in newton.inner_html()
# Confirm that all fieldsets are rendered correctly
poly = newton.wait_for_selector("#poly")
assert poly.input_value() == "z**3 - 2*z + 2"
coef = newton.wait_for_selector("#coef")
assert coef.input_value() == "1"
# Let's now change some x/y values to confirm that they
# are editable (is it the best way to test this?)
x0 = newton.wait_for_selector("#x0")
y0 = newton.wait_for_selector("#y0")
x0.fill("50")
assert x0.input_value() == "50"
y0.fill("-25")
assert y0.input_value() == "-25"
# This was the first computation with the default values
assert self.console.log.lines[-2] == "Computing Newton set ..."
# Confirm that changing the input values, triggered a new computation
assert self.console.log.lines[-1] == "Computing Newton set ..."
def test_panel(self):
# XXX improve this test
self.goto("examples/panel.html")
self.wait_for_pyscript()
assert self.page.title() == "Panel Example"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
slider_title = self.page.wait_for_selector(".bk-slider-title")
assert slider_title.inner_text() == "Amplitude: 0"
slider_result = self.page.wait_for_selector(".bk-clearfix")
assert slider_result.inner_text() == "Amplitude is: 0"
amplitude_bar = self.page.wait_for_selector(".noUi-connects")
amplitude_bar.click()
# Let's confirm that slider title changed
assert slider_title.inner_text() == "Amplitude: 5"
def test_panel_deckgl(self):
# XXX improve this test
@@ -190,7 +273,6 @@ class TestExamples(PyScriptTest):
)
assert second_repl_result.text_content() == "4"
@pytest.mark.xfail(reason="Test seems flaky")
def test_repl2(self):
self.goto("examples/repl2.html")
self.wait_for_pyscript()

View File

@@ -0,0 +1,25 @@
import { jest } from "@jest/globals"
import { PyBox } from "../../src/components/pybox"
customElements.define('py-box', PyBox)
describe('PyBox', () => {
let instance: PyBox;
beforeEach(() => {
instance = new PyBox();
})
it('PyBox instantiates correctly', async () => {
expect(instance).toBeInstanceOf(PyBox)
})
it("test connectedCallback creates pybox div", async () => {
expect(instance.innerHTML).toBe("")
instance.connectedCallback()
expect(instance.innerHTML).toBe('<div class=\"py-box\"></div>')
})
})

View File

@@ -14,7 +14,7 @@ describe('PyButton', () => {
expect(instance).toBeInstanceOf(PyButton);
});
it('confirm that runAfterRuntimeInitialized is called', async () => {
it('confirm that runAfterRuntimeInitialized is called', async () => {
const mockedRunAfterRuntimeInitialized = jest
.spyOn(instance, 'runAfterRuntimeInitialized')
.mockImplementation(jest.fn());

View File

@@ -1,13 +1,17 @@
import { jest } from '@jest/globals';
import type { AppConfig, RuntimeConfig } from '../../src/runtime';
import { PyConfig } from '../../src/components/pyconfig';
// inspired by trump typos
const covfefeConfig = {
"name": "covfefe",
"runtimes": [{
"src": "/demo/covfefe.js",
"name": "covfefe",
"lang": "covfefe"
}],
"wonerful": "discgrace"
name: 'covfefe',
runtimes: [
{
src: '/demo/covfefe.js',
name: 'covfefe',
lang: 'covfefe',
},
],
wonerful: 'discgrace',
};
const covfefeConfigToml = `
@@ -21,20 +25,18 @@ name = "covfefe"
lang = "covfefe"
`;
import {jest} from '@jest/globals';
customElements.define('py-config', PyConfig);
describe('PyConfig', () => {
let instance: PyConfig;
const xhrMockClass = () => ({
open : jest.fn(),
send : jest.fn(),
responseText : JSON.stringify(covfefeConfig)
open: jest.fn(),
send: jest.fn(),
responseText: JSON.stringify(covfefeConfig),
});
// @ts-ignore
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
beforeEach(() => {
instance = new PyConfig();
@@ -47,57 +49,57 @@ describe('PyConfig', () => {
it('should load runtime from config and set as script src', () => {
instance.values = covfefeConfig;
instance.loadRuntimes();
expect(document.scripts[0].src).toBe("http://localhost/demo/covfefe.js");
expect(document.scripts[0].src).toBe('http://localhost/demo/covfefe.js');
});
it('should load the default config', ()=> {
it('should load the default config', () => {
instance.connectedCallback();
expect(instance.values.name).toBe("pyscript");
expect(instance.values.author_email).toBe("foo@bar.com");
expect(instance.values.name).toBe('pyscript');
expect(instance.values.author_email).toBe('foo@bar.com');
expect(instance.values.pyscript?.time).not.toBeNull();
// @ts-ignore
expect(instance.values.runtimes[0].lang).toBe("python");
expect(instance.values.runtimes[0].lang).toBe('python');
});
it('should load the JSON config from inline', ()=> {
instance.setAttribute("type", "json");
it('should load the JSON config from inline', () => {
instance.setAttribute('type', 'json');
instance.innerHTML = JSON.stringify(covfefeConfig);
instance.connectedCallback();
// @ts-ignore
expect(instance.values.runtimes[0].lang).toBe("covfefe");
expect(instance.values.runtimes[0].lang).toBe('covfefe');
expect(instance.values.pyscript?.time).not.toBeNull();
// version wasn't present in `inline config` but is still set due to merging with default
expect(instance.values.version).toBe("0.1");
expect(instance.values.version).toBe('0.1');
});
it('should load the JSON config from src attribute', ()=> {
instance.setAttribute("type", "json");
instance.setAttribute("src", "/covfefe.json");
it('should load the JSON config from src attribute', () => {
instance.setAttribute('type', 'json');
instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback();
// @ts-ignore
expect(instance.values.runtimes[0].lang).toBe("covfefe");
expect(instance.values.runtimes[0].lang).toBe('covfefe');
expect(instance.values.pyscript?.time).not.toBeNull();
// wonerful is an extra key supplied by the user and is unaffected by merging process
expect(instance.values.wonerful).toBe("discgrace");
expect(instance.values.wonerful).toBe('discgrace');
// version wasn't present in `config from src` but is still set due to merging with default
expect(instance.values.version).toBe("0.1");
expect(instance.values.version).toBe('0.1');
});
it('should load the JSON config from both inline and src', ()=> {
instance.setAttribute("type", "json");
instance.innerHTML = JSON.stringify({"version": "0.2a", "wonerful": "highjacked"});
instance.setAttribute("src", "/covfefe.json");
it('should load the JSON config from both inline and src', () => {
instance.setAttribute('type', 'json');
instance.innerHTML = JSON.stringify({ version: '0.2a', wonerful: 'highjacked' });
instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback();
// @ts-ignore
expect(instance.values.runtimes[0].lang).toBe("covfefe");
expect(instance.values.runtimes[0].lang).toBe('covfefe');
expect(instance.values.pyscript?.time).not.toBeNull();
// config from src had an extra key "wonerful" with value "discgrace"
// inline config had the same extra key "wonerful" with value "highjacked"
// the merge process works for extra keys that clash as well
// so the final value is "highjacked" since inline takes precedence over src
expect(instance.values.wonerful).toBe("highjacked");
expect(instance.values.wonerful).toBe('highjacked');
// version wasn't present in `config from src` but is still set due to merging with default and inline
expect(instance.values.version).toBe("0.2a");
expect(instance.values.version).toBe('0.2a');
});
it('should be able to load an inline TOML config', () => {
@@ -105,11 +107,11 @@ describe('PyConfig', () => {
instance.innerHTML = covfefeConfigToml;
instance.connectedCallback();
// @ts-ignore
expect(instance.values.runtimes[0].lang).toBe("covfefe");
expect(instance.values.runtimes[0].lang).toBe('covfefe');
expect(instance.values.pyscript?.time).not.toBeNull();
// version wasn't present in `inline config` but is still set due to merging with default
expect(instance.values.version).toBe("0.1");
expect(instance.values.wonerful).toBe("highjacked");
expect(instance.values.version).toBe('0.1');
expect(instance.values.wonerful).toBe('highjacked');
});
it.failing('should NOT be able to load an inline config in JSON format with type as TOML', () => {
@@ -118,21 +120,56 @@ describe('PyConfig', () => {
});
it.failing('should NOT be able to load an inline config in TOML format with type as JSON', () => {
instance.setAttribute("type", "json");
instance.setAttribute('type', 'json');
instance.innerHTML = covfefeConfigToml;
instance.connectedCallback();
});
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => {
instance.innerHTML = covfefeConfigToml;
instance.setAttribute("src", "/covfefe.json");
instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback();
});
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => {
instance.setAttribute("type", "json");
instance.setAttribute('type', 'json');
instance.innerHTML = covfefeConfigToml;
instance.setAttribute("src", "/covfefe.json");
instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback();
});
it('connectedCallback should call loadRuntimes', async () => {
const mockedMethod = jest.fn();
instance.loadRuntimes = mockedMethod;
instance.connectedCallback();
expect(mockedMethod).toHaveBeenCalled();
});
it('confirm connectedCallback happy path', async () => {
const mockedMethod = jest.fn();
instance.loadRuntimes = mockedMethod;
instance.innerHTML = 'test';
instance.connectedCallback();
expect(instance.code).toBe('test');
expect(instance.values['0']).toBe('test');
});
it('log should add new message to the page', async () => {
// details are undefined, so let's create a div for it
instance.details = document.createElement('div');
instance.log('this is a log');
// @ts-ignore: typescript complains about accessing innerText
expect(instance.details.childNodes[0].innerText).toBe('this is a log');
});
it('confirm that calling close would call this.remove', async () => {
instance.remove = jest.fn();
instance.close();
expect(instance.remove).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,66 @@
import { jest } from "@jest/globals"
import { PyInputBox } from "../../src/components/pyinputbox"
customElements.define('py-inputbox', PyInputBox)
describe("PyInputBox", () => {
let instance: PyInputBox;
beforeEach(() => {
instance = new PyInputBox()
instance.runAfterRuntimeInitialized = jest.fn();
})
it("PyInputBox instantiates correctly", async () => {
expect(instance).toBeInstanceOf(PyInputBox)
})
it('connectedCallback gets or sets a new id', async () => {
expect(instance.id).toBe('');
instance.connectedCallback();
const instanceId = instance.id;
// id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container
expect(instanceId).toMatch(/py-(\w+-){1,5}container/);
// calling checkId directly should return the same id
instance.checkId();
expect(instance.id).toEqual(instanceId);
});
it('confirm that runAfterRuntimeInitialized is called', async () => {
const mockedRunAfterRuntimeInitialized = jest
.spyOn(instance, 'runAfterRuntimeInitialized')
.mockImplementation(jest.fn());
instance.connectedCallback();
expect(mockedRunAfterRuntimeInitialized).toHaveBeenCalled();
})
it('onCallback sets mount_name based on id', async () => {
expect(instance.id).toBe('');
expect(instance.mount_name).toBe(undefined);
instance.connectedCallback();
const instanceId = instance.id;
expect(instanceId).toMatch(/py-(\w+-){1,5}container/);
expect(instance.mount_name).toBe(instanceId.replace('-container', '').split('-').join('_'));
});
it('onCallback updates on_keypress code and function name ', async () => {
expect(instance.code).toBe(undefined);
expect(instance.innerHTML).toBe('');
instance.innerHTML = '\ndef on_keypress(e):\n';
instance.connectedCallback();
expect(instance.code).toMatch(/def\son_keypress_py_(\w+)\(e\)/);
expect(instance.innerHTML).toContain('<input type="text" class="py-input"');
});
})

View File

@@ -0,0 +1,52 @@
import { jest } from '@jest/globals';
import { PyLoader } from "../../src/components/pyloader"
import { getLogger } from "../../src/logger"
customElements.define('py-loader', PyLoader);
describe('PyLoader', () => {
let instance: PyLoader;
const logger = getLogger("py-loader")
beforeEach(() => {
instance = new PyLoader();
logger.info = jest.fn()
})
it('PyLoader instantiates correctly', async () => {
expect (instance).toBeInstanceOf(PyLoader);
})
it('connectedCallback adds splash screen', async () => {
// innerHTML should be empty
expect(instance.innerHTML).toBe("")
instance.connectedCallback();
// This is just checking that we have some ids or class names
expect(instance.innerHTML).toContain('pyscript_loading_splash')
expect(instance.innerHTML).toContain("spinner")
expect(instance.mount_name).toBe("")
})
it('confirm calling log will log to console and page', () => {
const element = document.createElement('div')
element.setAttribute("id", "pyscript-operation-details")
instance.details = element
instance.log("Hello, world!")
const printedLog = element.getElementsByTagName('p')
expect(logger.info).toHaveBeenCalledWith("Hello, world!")
expect(printedLog[0].innerText).toBe("Hello, world!")
})
it('confirm that calling close removes element', async () => {
instance.remove = jest.fn()
instance.close()
expect(logger.info).toHaveBeenCalledWith("Closing")
expect(instance.remove).toHaveBeenCalled()
})
})

View File

@@ -1,4 +1,5 @@
import 'jest';
import { PyRepl } from '../../src/components/pyrepl';
customElements.define('py-repl', PyRepl);
@@ -13,4 +14,52 @@ describe('PyRepl', () => {
expect(instance).toBeInstanceOf(PyRepl);
});
it('confirm that codemirror editor is available', async () => {
// We are assuming that if editorNode has the 'editor-box' class
// then the div was created properly.
expect(instance.editorNode.getAttribute('class')).toBe("editor-box")
})
it("connectedCallback gets or sets new id", async () => {
expect(instance.id).toBe("")
instance.connectedCallback()
const instanceId = instance.id;
// id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d
expect(instanceId).toMatch(/py-(\w+-){1,4}\w+/);
// calling checkId directly should return the same id
instance.checkId();
expect(instance.id).toEqual(instanceId);
})
it('confirm that calling connectedCallback renders the expected elements', async () => {
expect(instance.innerHTML).toBe("")
instance.innerHTML = "<p>Hello</p>"
instance.connectedCallback()
expect(instance.code).toBe("<p>Hello</p>")
expect(instance.editorNode.id).toBe("code-editor")
// Just check that the button was created
expect(instance.btnRun.getAttribute("class")).toBe("absolute repl-play-button")
const editorNode = instance.editorNode.innerHTML
expect(editorNode).toContain("Python Script Run Button")
// Confirm that our innerHTML is set as well
expect(editorNode).toContain("Hello")
})
it("confirm that addToOutput updates output element", async () => {
expect(instance.outputElement).toBe(undefined)
// This is just to avoid throwing the test since outputElement is undefined
instance.outputElement = document.createElement("div")
instance.addToOutput("Hello, World!")
expect(instance.outputElement.innerHTML).toBe("<div>Hello, World!</div>")
expect(instance.outputElement.hidden).toBe(false)
})
});

View File

@@ -0,0 +1,76 @@
import { jest } from "@jest/globals"
import { PyScript } from "../../src/components/pyscript"
customElements.define('py-script', PyScript)
describe('PyScript', () => {
let instance: PyScript;
beforeEach(() => {
instance = new PyScript();
})
it('PyScript instantiates correctly', async () => {
expect(instance).toBeInstanceOf(PyScript)
})
it('connectedCallback gets or sets a new id', async () => {
expect(instance.id).toBe('');
instance.connectedCallback();
const instanceId = instance.id;
// id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container
expect(instanceId).toMatch(/py-(\w+-){1,5}/);
// calling checkId directly should return the same id
instance.checkId();
expect(instance.id).toEqual(instanceId);
});
it('connectedCallback creates output div', async () => {
instance.connectedCallback();
expect(instance.innerHTML).toContain('<div class="output">')
})
it('confirm that outputElement has std-out id element', async () => {
expect(instance.outputElement).toBe(undefined);
instance.setAttribute('id', 'std-out')
instance.connectedCallback();
expect(instance.outputElement.getAttribute('id')).toBe("std-out")
})
it('confirm that std-err id element sets errorElement', async () => {
expect(instance.outputElement).toBe(undefined);
instance.setAttribute('id', 'std-err')
instance.connectedCallback();
// We should have an errorElement
expect(instance.errorElement.getAttribute('id')).toBe("std-err")
})
it('test output attribute path', async () => {
expect(instance.outputElement).toBe(undefined);
expect(instance.errorElement).toBe(undefined)
const createdOutput = document.createElement("output")
instance.setAttribute('output', 'output')
instance.connectedCallback();
expect(instance.innerHTML).toBe('<div class="output"></div>')
})
it('getSourceFromElement returns decoded html', async () => {
instance.innerHTML = "<p>Hello</p>"
instance.connectedCallback();
const source = instance.getSourceFromElement();
expect(source).toBe("<p>Hello</p>")
})
})

View File

@@ -0,0 +1,40 @@
import { jest } from "@jest/globals";
import { PyTitle } from "../../src/components/pytitle"
customElements.define("py-title", PyTitle);
describe("PyTitle", () => {
let instance: PyTitle;
beforeEach(() => {
instance = new PyTitle();
})
it("PyTitle instantiates correctly", async () => {
expect(instance).toBeInstanceOf(PyTitle);
})
it("test connectedCallback defaults", async () => {
instance.connectedCallback();
expect(instance.label).toBe("")
expect(instance.mount_name).toBe("")
expect(instance.innerHTML).toBe(`<div class=\"py-title\" id=\"\"><h1></h1></div>`)
})
it("label renders correctly on the page and updates id", async () => {
instance.innerHTML = "Hello, world!"
// We need this to test mount_name works properly since connectedCallback
// doesn't automatically call checkId (should it?)
instance.checkId();
instance.connectedCallback();
expect(instance.label).toBe("Hello, world!")
// mount_name should be similar to: py_be025f4c_2150_7f2a_1a85_af92970c2a0e
expect(instance.mount_name).toMatch(/py_(\w+_){1,5}/);
expect(instance.innerHTML).toContain("<h1>Hello, world!</h1>")
})
})