PMT Tools
pmt_tools provides test-oriented helpers for interacting with the interactive components generated by PMT/MkDocs pages.
The package is built around a simple idea:
page
└── runners
├── IDEs
├── terminals
├── Python buttons
└── auto-run blocks
The package turns these DOM components into Python objects that can be queried, interacted with, and validated through selenium-query.
The main objects exposed by the package are:
BasePmtPage: the page-level entry point;Ide: interaction with PMT IDEs;Terminal: interaction with terminals;PyBtn: interaction with Python buttons;Runner: the common abstraction for executable page components;AutoRun: interaction withrunmacros elements;Qcm: interaction with QCM components;TabbedContent: interaction with MkDocs-Material tabbed contents.
BasePmtPage
BasePmtPage extends selenium_query.BaseSeleniumPage with PMT-specific page loading and runner discovery.
A page class should inherit from it and define its URL, following the same requirements as selenium_query.BaseSeleniumPage. See the package documentation if needed.
from pmt_tools import BasePmtPage
class MyPage(BasePmtPage):
URL = "my-page"
The page readiness condition is PMT-specific. A page is considered ready when the relevant subscription flags have completed:
- All subscriptions have been done
- MCQs have been updated (if needed)
- Mathjax has been executed (if needed)
Once the page data is gathered, BasePmtPage discovers the runners present on the page and store them in two class level objects:
MyPage.runners: list[Runner]
MyPage.runners_by_class: dict[str, list[Runner]]
IMPORTANT: Runners objects should never be instantiated manually by the user. Systematically use the BasePmt.base_gather_data logistic to make everything automatic.
The page-level abstraction is therefore:
BasePmtPage
↓
load PMT page
↓
wait for PMT initialization
↓
discover page runners
↓
wrap each runner in its specialized Python object
IdeConfig, CorrRemProfile and CheckBtnColor
These are configuration/value helpers used by Ide.
IdeConfig
IdeConfig describes the configuration of an IDE:
@dataclass
class IdeConfig:
count: Union[float,int,None]
has_check_btn: bool
has_corr_and_reveal_btns: bool
revealed: bool = False
@property
def has_counter(self):
return self.count is not None
| Attribute | Description |
|---|---|
count |
Number of remaining attempts, or inf or None if no counter is present. |
has_check_btn |
Whether the IDE has a check button. |
has_corr_and_reveal_btns |
Whether correction and reveal buttons are available. |
is_revealed |
Whether the solution has been revealed. |
has_counter |
Whether the IDE has a counter. |
Ide
Ide is the complete IDE abstraction.
It combines the behavior defined by the IDE-related classes in ide.py:
Ide
├── common IDE behavior
├── code editor behavior
├── correction / remarks / counter behavior
├── split-screen / full-screen behavior
├── history behavior
├── `Terminal` behaviors
├── `Runner` behaviors
└── button
├── comment
├── two_cols
├── full_screen
├── play
├── check
├── download
├── upload
├── restart
├── save
├── zip
├── corr
├── reveal
└── Terminal buttons...
A typical IDE is obtained automatically from a BasePmtPage:
ides = cls.runners_by_class["py_mk_ide"]
The object then provides a high-level API for interacting with the IDE. See the Terminal class for interactions with the terminal of an IDE.
Reading and writing the editor content
| Signature | Description |
|---|---|
ide.get_editor_code() |
Gets the current code in the editor. |
ide.set_editor_code(code) |
Replaces the editor's code. |
ide.clear_editor() |
Clears the editor. |
ide.check_code_editor(code) |
Checks the editor's code exactly. |
ide.check_code_editor(code, contains=True) |
Checks that the editor contains the given code. |
ide.clean_then_type_in_editor(code) |
Clears the editor, then types the given code. |
ide.editor_shortcut(shortcut) |
Sends a keyboard shortcut to the editor. |
Validations
| Signature | Description |
|---|---|
ide.validate_and_check_btn_becomes(color:CheckBtnColor) |
Clicks the validation button and checks that it becomes the expected color. |
ide.check_validation_btn_color(color:CheckBtnColor) |
Only checks the current color of the validation button. |
ide.has_orange_box(expected=True) |
Checks whether the IDE validation button displays the orange “modified code” box. |
ide.is_dirty(expected=True) |
Checks whether the IDE is in a dirty state (internally). |
ide.check_count(count) |
Checks the number of remaining attempts. count can be an integer, inf for unlimited attempts, or None for no counter. |
ide.click_corr_rem() |
Opens or closes the correction/remarks section and returns its <details> as a Getter. |
ide.check_corr_rem_config(profile:CorrRemProfile, finally_close=False) |
Checks the correction/remarks configuration and optionally closes the section afterward. |
CorrRemProfile
CorrRemProfile describes what the solution area should contain:
| Value | Meaning |
|---|---|
CorrRemProfile.hidden |
Hidden |
CorrRemProfile.none |
No solution/correction content |
CorrRemProfile.corr |
Solution |
CorrRemProfile.rem |
Remarks |
CorrRemProfile.corr_rem |
Solution + remarks |
They are typically used with the check_corr_rem_config(...) method.
CheckBtnColor
CheckBtnColor contains the expected visual state of the validation button:
| Value | Color |
|---|---|
CheckBtnColor.success |
green |
CheckBtnColor.failure |
red |
CheckBtnColor.default |
none |
CheckBtnColor.teacher |
blue |
Validations history
| Signature | Description |
|---|---|
ide.get_history() |
Opens the validation history and returns its items. |
ide.check_history_items_count(count) |
Checks the number of items in the validation history. |
ide.check_history_item(teacher=True) |
Checks a history item as a teacher entry. |
ide.check_history_item() |
Checks a dated history entry. |
ide.cleanup_history(keep) |
Cleans the history while keeping the specified number of first entries. |
Full-screen and split-screen modes
| Signature | Description |
|---|---|
ide.is_full_screen(expected) |
Checks whether the IDE is in full-screen mode. |
ide.check_btn_full_screen(enter=True) |
Checks the full-screen button behavior. |
ide.check_shortcut_full_screen(enter=True) |
Checks the full-screen keyboard shortcut behavior. |
ide.is_split_screen(expected) |
Checks whether the IDE is in split-screen mode. |
ide.check_btn_split_screen(enter=True) |
Checks the split-screen button behavior. |
ide.check_shortcut_split_screen(enter=True) |
Checks the split-screen keyboard shortcut behavior. |
ide.check_placeholder_is_in_place() |
Checks the placeholder position after entering split-screen mode. |
ide.check_is_back_in_place() |
Checks that the IDE is back in its original position. |
ide.check_is_split_full_height() |
Checks that the split-screen layout uses the full available height. |
Various helpers
| Signature | Description |
|---|---|
ide.check_has_src_hash_msg(has_it=True) |
Checks whether the IDE displays a source-hash warning. |
ide.zip_upload() |
Triggers the IDE's ZIP upload mechanism. |
ide.has_priority() |
Checks whether the IDE has priority when several IDEs are grouped together. |
ide.reset() |
Resets the underlying IDE through its JavaScript API (same as the restart button, without the confirmation). |
ide.set_python_global_N(value) |
Sets the Python global N through Pyodide (see PMT pylibs module). |
Terminal
Terminal is the complete terminal abstraction.
It combines:
Terminal
├── common terminal behavior
├── text selection behavior
├── `Runner` behaviors
└── button
├── cut_term
└── stdout_wraps
A terminal is discovered automatically from a BasePmtPage. They can be extracted from BasePmtPage.runners_by_class using the html class names term_solo for isolated terminals or py_mk_terminal for terminals embedded within IDE elements.
| Signature | Description |
|---|---|
term.get_term_content() |
Gets the current terminal content, including the prompt. |
term.check_term_content(content, contains=False) |
Checks the terminal content exactly or, with contains=True, checks that it contains the given text. |
term.exec_term_cmd(command) |
Executes a command directly through the terminal's JavaScript runner (hence, without typing it in the terminal). |
term.clean_then_type_in_terminal(command) |
Clears the terminal, types the command, and executes it. |
term.run_command(command) |
Types a command into the terminal and presses Enter. |
term.check_current_cmd(command) |
Checks the command currently entered in the terminal without executing it. |
term.check_command(command, expected, contains=False) |
Executes a command and checks its resulting terminal content; if command=None, checks the existing content without running a command. |
term.send_keys_terminal(*keys, execute=False) |
Sends keys to the terminal, optionally executing them. Enter or Return also triggers execution. |
Selecting terminal text
Terminal also provides helpers for testing text selection behaviors.
The prompt can be double-clicked:
terminal.double_click_prompt()
The cursor can be placed at one of the supported positions:
terminal.set_cursor(0) # beginning of the line
terminal.set_cursor(1) # middle of the line
terminal.set_cursor(-1) # end of the line
A selection can then be made by providing the characters after which the cursor should be put, both for the beginning and ending points:
terminal.select(
"output.0.abc", # Start: first line of output, after "abc"
"output.3.def", # End: fourth line of output, after "def"
)
An exception is raised if the specified "points" are invalid.
The selection API uses a small dot-separated notation:
"section[.line].characters"
| Element | Description |
|---|---|
section |
either output, prompt or cmd |
line |
The index of the line in the section (can be negative, optional if one line only) |
characters |
The string of characters after which (from left) |
Examples:
prompt.>>
cmd.-1.abcde
output.5.some text
cmd.abcde
output.-2."Use quotes for dots..."
Once the text is selected, it can then be retrieved or its content checked:
selected = terminal.get_term_selected_text()
terminal.check_term_selected_text("expected text")
Runner
Runner is the common abstraction for executable PMT components. It wraps a selenium_query.Getter, with relay method to the main/usual Getter logistic. They also provide facilitators to handle running JS methods of the corresponding PTM objects, with various context managers.
Buttons
A Runner (and also any child class) always provide an interface to interact easily with all the buttons it holds in its interface.
These are accessible through runner.button.xxx where xxx is the button identifier (see the related classes), and provide methods to simplify their use and testing.
| Signature | Description |
|---|---|
| `button.click(extra_delay=None) -> Button | Alert` |
button.right_click() -> Button |
Applies right click operation on the button. |
button.check_tip_text(exp:str, contains=False, floating=False) |
Check tooltips messages. |
button.check_capytale_chip(txt_or_int, msg="") |
Checks the Capytale counter displayed on the button, or verifies that it is absent when txt_or_int is falsy. |
-
click(...):- Returns the Button itself, or an Alert object if one is expected on click. The user will have to handle the Alert.
- Can apply
extra_delayin addition to the default waiting behaviors.
-
check_tip_text(...):- If
contains=True,expmust be contained in the tooltip text, instead of matching it exactly. - Use
floating=Trueto check "PMT bare tips". In this case, the Button automatically scroll and move the pointer where appropriate to display the tooltip.
- If
-
check_capytale_chip:To use only when testing CodEx integration in Capytale.
msgis an optional extra assertion message.
runner.run_js(...)
A Runner object is aware of it's base element html id, and, if the website is built with pyodide_macros._dev_mode: true, the Runner can automatically access the JS object when using run_js by using {js_runner} in the code passed as argument. hence, something like this is possible:
runner.run_js("return {js_runner}.isDirty")
See selenium-query documentation for more details about how to use the run_js method.
Synchronisation helper
Each runner also provides a basic synchronization context manager, which might help implement various waiting mechanisms. The Runner automatically handles the internal logistic, making sure the JS runner has ended executions before releasing the context.
with runner.wait_executions_done():
...
IMPORTANT: considering PMT executions, they are considered done just before any post or post_term section is executed (see PMT's documentation).
Miscellaneous
-
runner.rename("my-runner")can ease debugging: a Runnerreprcan use the value passed torename(...)instead of the css path used to find the object in the DOM (seeGetter). -
runner.pause(seconds:float)can be used directly (no need for a terminal.go). -
In normal page usage,
Runnerobjects are generally created automatically with: `Runner.from_page(MyPage)```This discovers:
- IDEs;
- isolated terminals;
- Python buttons;
- auto-run components.
PyBtn
They are generally gathered in the page under the py_mk_py_btn html class name.
PyBtn
├── `Runner` behaviors
└── button
└── play
Aside of the base Runner interface, they also provide this specialized helper to check that clicking the button produces the expected alert (for the buttons that do so):
py_btn.check_alert_contains_on_click("Expected alert message")
AutoRun
They are generally gathered in the page under the py_mk_auto_run html class name.
It is a Runner specialization without additional public behavior of its own, and is mainly useful because Runner.from_page(...) can recognize an auto-run block and wrap it in the appropriate Python object.
QcmStructure
QcmStructure describes the expected structure of a QCM.
For each question, it records:
from pmt_tools import QcmStructure
structure = QcmStructure(
items_per_question=[4, 3],
are_squares=[False, True],
correct=[2, (1, 3)],
shuffle_questions=True,
shuffled_items=[False, True],
)
The values mean:
- question 1 has 4 choices;
- question 2 has 3 choices;
- question 1 is single-choice;
- question 2 is multiple-choice;
- the correct answer for question 1 is item number 2;
- the correct answers for question 2 are items 1 and 3;
- questions may be shuffled;
- items of question 2 may be shuffled.
The correct values use question/item numbers, not zero-based indexes.
The lists describing the questions must all have the same length:
len(items_per_question)
== len(are_squares)
== len(shuffled_items)
An optional mask can also be specified:
structure = QcmStructure(
items_per_question=[4],
are_squares=[False],
correct=[2],
shuffle_questions=False,
shuffled_items=[False],
mask=True,
)
Qcm
Qcm represents a PMT QCM component.
| Signature | Description |
|---|---|
Qcm.from_page(page) -> List[Qcm] |
Gets all QCMs on a page. |
qcm.get_questions() -> Getter |
Gets the QCM questions. |
qcm.get_items(question: Getter) -> Getter |
Gets the items of a question. |
qcm.get_q_i_2D_array(target: str = "") -> List[List[Getter]] |
Gets questions and items as a 2D array. |
qcm.get_rems() -> Getter |
Gets the correction/remarks elements. |
qcm.select(q_and_i: str) |
Selects items using question.item notation. |
| `qcm.check_selected(selected: str, svg_class: str | Dict[str,str] = None)` |
qcm.validate() |
Validates the QCM (as in, "evaluate it"). |
qcm.validate_if_not_yet() |
Validates the QCM if necessary. |
qcm.reset() |
Resets the QCM. |
qcm.check_counter(expected: int) |
Checks the QCM counter/result. |
qcm.snapshot() -> str |
Creates a snapshot of the QCM's question/item IDs. |
-
get_q_i_2D_array:targetallow to target children elements of the QCM items (as a css selector). -
get_rems: The QCM must already be evaluated, otherwise the REMs cannot be found. -
select: the string is giving POSITIONS, not indices. Example: "1.1 1.2 2.4" selects items 1 and 2 of question 1 and item 4 of question 2. -
check_selectedbehaves the same way asselectfor the first argument.svg_classallows to check the behaviors of the svg elements and can be provided in various ways (string, dict ofq.istrings as keys). See implementation details.Examples:
qcm.check_selected("1.2 2.1") qcm.check_selected("1.2 2.1", svg_class="my-class") qcm.check_selected( "1.2 2.1", svg_class={"1.2": "correct","2.1": "missed"} )
-
snapshot: a QCM snapshot records the IDs of its questions and items. This is useful to test randomness.
TabbedContent
TabbedContent represents a MkDocs tabbed block (MkDocs-Material/PyMdown-Extension === "..." syntaxes).
| Signature | Description |
|---|---|
TabbedContent.from_page(page) -> List[TabbedContent] |
Gets all tabbed blocks on a page. |
tabbed.labels -> Getter |
Gets the tab labels. |
tabbed.contents -> Getter |
Gets the tab contents. |
tabbed.click(index) -> None |
Selects a tab and checks that its content is visible. |
tabbed.label -> Getter |
Gets the currently selected tab's label. |
tabbed.content -> Getter |
Gets the currently selected tab's content. |
tabbed.check_visible(index) -> None |
Checks that a tab's content is visible. |
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pmt_tools-0.2.1.tar.gz.
File metadata
- Download URL: pmt_tools-0.2.1.tar.gz
- Upload date:
- Size: 38.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.10.10 Linux/6.8.0-136-generic
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad2d36fede8a79b322208a4c1e3e0e20e57b81a9106d977d55d4bb71a9ea5dbb
|
|
| MD5 |
e70030e9506db9b5b7de0e22b7c6d408
|
|
| BLAKE2b-256 |
8d5f02719143e84a7d2a2154a8810bb40c11d845c3a993d878d6e8c3c795828a
|
File details
Details for the file pmt_tools-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pmt_tools-0.2.1-py3-none-any.whl
- Upload date:
- Size: 43.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.10.10 Linux/6.8.0-136-generic
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57782416c85fc8f1b27ff21a06bf3b0d9718881d9988716d16a3ddb1d83e08b4
|
|
| MD5 |
6005e2c8e6c9a3f8d12d15998646fead
|
|
| BLAKE2b-256 |
12d37b563aba9cff9a8261d63d5a0181ae210d4aebb6f92c5df7a4353fac9823
|