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

@@ -48,13 +48,13 @@
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"d3": "https://cdn.skypack.dev/d3@7" "d3": "https://cdn.skypack.dev/pin/d3@v7.6.1-1Q0NZ0WZnbYeSjDusJT3/mode=imports,min/optimized/d3.js"
} }
} }
</script> </script>
<script type="module"> <script type="module">
import * as d3 from "https://cdn.skypack.dev/d3@7"; import * as d3 from "https://cdn.skypack.dev/pin/d3@v7.6.1-1Q0NZ0WZnbYeSjDusJT3/mode=imports,min/optimized/d3.js";
const fruits = [ const fruits = [
{name: "🍊", count: 21}, {name: "🍊", count: 21},

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,8 +1,13 @@
import base64
import io
import math import math
import os
import re import re
import time import time
import numpy as np
import pytest import pytest
from PIL import Image
from .support import ROOT, PyScriptTest from .support import ROOT, PyScriptTest
@@ -80,7 +85,6 @@ class TestExamples(PyScriptTest):
wait_for_render(self.page, "*", '<canvas.*?class=\\"marks\\".*?>') wait_for_render(self.page, "*", '<canvas.*?class=\\"marks\\".*?>')
save_as_png_link = self.page.locator("text=Save as PNG") save_as_png_link = self.page.locator("text=Save as PNG")
see_source_link = self.page.locator("text=View Source") see_source_link = self.page.locator("text=View Source")
# These shouldn't be visible since we didn't click the menu # These shouldn't be visible since we didn't click the menu
assert not save_as_png_link.is_visible() assert not save_as_png_link.is_visible()
assert not see_source_link.is_visible() assert not see_source_link.is_visible()
@@ -105,32 +109,66 @@ class TestExamples(PyScriptTest):
assert self.page.title() == "Bokeh Example" assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>') wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
@pytest.mark.xfail(reason="Flaky test #759")
def test_d3(self): def test_d3(self):
# XXX improve this test
self.goto("examples/d3.html") self.goto("examples/d3.html")
self.wait_for_pyscript() self.wait_for_pyscript()
assert ( assert (
self.page.title() == "d3: JavaScript & PyScript visualizations side-by-side" self.page.title() == "d3: JavaScript & PyScript visualizations side-by-side"
) )
wait_for_render(self.page, "*", "<svg.*?>") 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): def test_folium(self):
# XXX improve this test
self.goto("examples/folium.html") self.goto("examples/folium.html")
self.wait_for_pyscript() self.wait_for_pyscript()
assert self.page.title() == "Folium" assert self.page.title() == "Folium"
wait_for_render(self.page, "*", "<iframe srcdoc=") 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): def test_matplotlib(self):
# XXX improve this test
self.goto("examples/matplotlib.html") self.goto("examples/matplotlib.html")
self.wait_for_pyscript() self.wait_for_pyscript()
assert self.page.title() == "Matplotlib" assert self.page.title() == "Matplotlib"
wait_for_render(self.page, "*", "<img src=['\"]data:image") 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): def test_numpy_canvas_fractals(self):
# XXX improve this test
self.goto("examples/numpy_canvas_fractals.html") self.goto("examples/numpy_canvas_fractals.html")
self.wait_for_pyscript() self.wait_for_pyscript()
assert ( assert (
@@ -141,12 +179,57 @@ class TestExamples(PyScriptTest):
self.page, "*", "<div.*?id=['\"](mandelbrot|julia|newton)['\"].*?>" 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): def test_panel(self):
# XXX improve this test
self.goto("examples/panel.html") self.goto("examples/panel.html")
self.wait_for_pyscript() self.wait_for_pyscript()
assert self.page.title() == "Panel Example" assert self.page.title() == "Panel Example"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>") 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): def test_panel_deckgl(self):
# XXX improve this test # XXX improve this test
@@ -190,7 +273,6 @@ class TestExamples(PyScriptTest):
) )
assert second_repl_result.text_content() == "4" assert second_repl_result.text_content() == "4"
@pytest.mark.xfail(reason="Test seems flaky")
def test_repl2(self): def test_repl2(self):
self.goto("examples/repl2.html") self.goto("examples/repl2.html")
self.wait_for_pyscript() 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); expect(instance).toBeInstanceOf(PyButton);
}); });
it('confirm that runAfterRuntimeInitialized is called', async () => { it('confirm that runAfterRuntimeInitialized is called', async () => {
const mockedRunAfterRuntimeInitialized = jest const mockedRunAfterRuntimeInitialized = jest
.spyOn(instance, 'runAfterRuntimeInitialized') .spyOn(instance, 'runAfterRuntimeInitialized')
.mockImplementation(jest.fn()); .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'; import { PyConfig } from '../../src/components/pyconfig';
// inspired by trump typos // inspired by trump typos
const covfefeConfig = { const covfefeConfig = {
"name": "covfefe", name: 'covfefe',
"runtimes": [{ runtimes: [
"src": "/demo/covfefe.js", {
"name": "covfefe", src: '/demo/covfefe.js',
"lang": "covfefe" name: 'covfefe',
}], lang: 'covfefe',
"wonerful": "discgrace" },
],
wonerful: 'discgrace',
}; };
const covfefeConfigToml = ` const covfefeConfigToml = `
@@ -21,20 +25,18 @@ name = "covfefe"
lang = "covfefe" lang = "covfefe"
`; `;
import {jest} from '@jest/globals';
customElements.define('py-config', PyConfig); customElements.define('py-config', PyConfig);
describe('PyConfig', () => { describe('PyConfig', () => {
let instance: PyConfig; let instance: PyConfig;
const xhrMockClass = () => ({ const xhrMockClass = () => ({
open : jest.fn(), open: jest.fn(),
send : jest.fn(), send: jest.fn(),
responseText : JSON.stringify(covfefeConfig) responseText: JSON.stringify(covfefeConfig),
}); });
// @ts-ignore // @ts-ignore
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass) window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
beforeEach(() => { beforeEach(() => {
instance = new PyConfig(); instance = new PyConfig();
@@ -47,57 +49,57 @@ describe('PyConfig', () => {
it('should load runtime from config and set as script src', () => { it('should load runtime from config and set as script src', () => {
instance.values = covfefeConfig; instance.values = covfefeConfig;
instance.loadRuntimes(); 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(); instance.connectedCallback();
expect(instance.values.name).toBe("pyscript"); expect(instance.values.name).toBe('pyscript');
expect(instance.values.author_email).toBe("foo@bar.com"); expect(instance.values.author_email).toBe('foo@bar.com');
expect(instance.values.pyscript?.time).not.toBeNull(); expect(instance.values.pyscript?.time).not.toBeNull();
// @ts-ignore // @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', ()=> { it('should load the JSON config from inline', () => {
instance.setAttribute("type", "json"); instance.setAttribute('type', 'json');
instance.innerHTML = JSON.stringify(covfefeConfig); instance.innerHTML = JSON.stringify(covfefeConfig);
instance.connectedCallback(); instance.connectedCallback();
// @ts-ignore // @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(); expect(instance.values.pyscript?.time).not.toBeNull();
// version wasn't present in `inline config` but is still set due to merging with default // 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', ()=> { it('should load the JSON config from src attribute', () => {
instance.setAttribute("type", "json"); instance.setAttribute('type', 'json');
instance.setAttribute("src", "/covfefe.json"); instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback(); instance.connectedCallback();
// @ts-ignore // @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(); expect(instance.values.pyscript?.time).not.toBeNull();
// wonerful is an extra key supplied by the user and is unaffected by merging process // 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 // 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', ()=> { it('should load the JSON config from both inline and src', () => {
instance.setAttribute("type", "json"); instance.setAttribute('type', 'json');
instance.innerHTML = JSON.stringify({"version": "0.2a", "wonerful": "highjacked"}); instance.innerHTML = JSON.stringify({ version: '0.2a', wonerful: 'highjacked' });
instance.setAttribute("src", "/covfefe.json"); instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback(); instance.connectedCallback();
// @ts-ignore // @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(); expect(instance.values.pyscript?.time).not.toBeNull();
// config from src had an extra key "wonerful" with value "discgrace" // config from src had an extra key "wonerful" with value "discgrace"
// inline config had the same extra key "wonerful" with value "highjacked" // inline config had the same extra key "wonerful" with value "highjacked"
// the merge process works for extra keys that clash as well // the merge process works for extra keys that clash as well
// so the final value is "highjacked" since inline takes precedence over src // 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 // 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', () => { it('should be able to load an inline TOML config', () => {
@@ -105,11 +107,11 @@ describe('PyConfig', () => {
instance.innerHTML = covfefeConfigToml; instance.innerHTML = covfefeConfigToml;
instance.connectedCallback(); instance.connectedCallback();
// @ts-ignore // @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(); expect(instance.values.pyscript?.time).not.toBeNull();
// version wasn't present in `inline config` but is still set due to merging with default // 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');
expect(instance.values.wonerful).toBe("highjacked"); expect(instance.values.wonerful).toBe('highjacked');
}); });
it.failing('should NOT be able to load an inline config in JSON format with type as TOML', () => { 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', () => { 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.innerHTML = covfefeConfigToml;
instance.connectedCallback(); instance.connectedCallback();
}); });
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => { 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.innerHTML = covfefeConfigToml;
instance.setAttribute("src", "/covfefe.json"); instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback(); instance.connectedCallback();
}); });
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => { 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.innerHTML = covfefeConfigToml;
instance.setAttribute("src", "/covfefe.json"); instance.setAttribute('src', '/covfefe.json');
instance.connectedCallback(); 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 'jest';
import { PyRepl } from '../../src/components/pyrepl'; import { PyRepl } from '../../src/components/pyrepl';
customElements.define('py-repl', PyRepl); customElements.define('py-repl', PyRepl);
@@ -13,4 +14,52 @@ describe('PyRepl', () => {
expect(instance).toBeInstanceOf(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>")
})
})