Skip to main content

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 with run macros 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_delay in addition to the default waiting behaviors.
  • check_tip_text(...):

    • If contains=True, exp must be contained in the tooltip text, instead of matching it exactly.
    • Use floating=True to check "PMT bare tips". In this case, the Button automatically scroll and move the pointer where appropriate to display the tooltip.
  • check_capytale_chip:

    To use only when testing CodEx integration in Capytale. msg is 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 Runner repr can use the value passed to rename(...) instead of the css path used to find the object in the DOM (see Getter).

  • runner.pause(seconds:float) can be used directly (no need for a terminal .go).

  • In normal page usage, Runner objects 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: target allow 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_selected behaves the same way as select for the first argument.

    svg_class allows to check the behaviors of the svg elements and can be provided in various ways (string, dict of q.i strings 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

pmt_tools-0.2.0.tar.gz (38.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pmt_tools-0.2.0-py3-none-any.whl (43.3 kB view details)

Uploaded Python 3

File details

Details for the file pmt_tools-0.2.0.tar.gz.

File metadata

  • Download URL: pmt_tools-0.2.0.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

Hashes for pmt_tools-0.2.0.tar.gz
Algorithm Hash digest
SHA256 57a0df3fc95d1cd35d06bcbb59efe8ad5b5fb07ce62f669512ee0f9236404a36
MD5 41212ca3daf6e1e88b5c1b5a7862a4c3
BLAKE2b-256 859133b708bf23e74c75859835a26741bfbf985f58dc49209ad119bb3b7cbdff

See more details on using hashes here.

File details

Details for the file pmt_tools-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pmt_tools-0.2.0-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

Hashes for pmt_tools-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b12865a964effb9eeaf46ec2b589f7fe3e05cf785010c6cd6f28ccee9b80b87f
MD5 66ef64ecbd7621609220e1046d91dfb9
BLAKE2b-256 06eed29054d2d296671a1f3279697f5f94e2b0d78ab53b46356108d39c72eb00

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page