diff --git a/.gitattributes b/.gitattributes index cbc003c5..2554d426 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ *.ipynb diff=jupyternotebook *.ipynb merge=jupyternotebook + +testdata/mfr-preview/*.pdf binary diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fae850b9..a2788614 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,6 +14,12 @@ - EMBER_IMAGE: - CAS_IMAGE: - MFR_IMAGE: +- MFR_REPOSITORY: +- MFR_BRANCH: +- UNOSERVER_IMAGE: + - WB_IMAGE: +- WB_REPOSITORY: +- WB_BRANCH: - EXCLUDE_NOTEBOOKS: diff --git a/.github/scripts/setup_rdm.sh b/.github/scripts/setup_rdm.sh index 21115103..250d8405 100644 --- a/.github/scripts/setup_rdm.sh +++ b/.github/scripts/setup_rdm.sh @@ -89,6 +89,14 @@ start_rdm_services() { SERVICES="$SERVICES minio" fi + # An MFR image with legacy dependencies converts office documents through the unoconv + # listener defined in the RDM-osf.io compose file, so it is started where previews are + # tested. An image with bundled dependencies brings its own conversion services + # (mfr_worker, unoserver) through the override instead. + if [ "${MFR_PREVIEW_ENABLED:-false}" = "true" ] && [ "${MFR_LAYOUT:-legacy}" = "legacy" ]; then + SERVICES="$SERVICES unoconv" + fi + echo "Starting services: $SERVICES" # Use start_services function @@ -225,6 +233,131 @@ BINDERHUB_EOF # Function to create docker-compose override with NII Cloud Operation images create_docker_override() { + # How the WaterButler and MFR images carry their Python dependencies, as detected from + # the images by the E2E workflow (WB_LAYOUT / MFR_LAYOUT) together with the directory + # that holds the interpreter's library and site-packages (WB_LIBDIR / MFR_LIBDIR, + # e.g. /usr/local/lib/python3.13): + # legacy - the image ships pip and the requirements volume is populated by running + # "invoke install", as the RDM-osf.io compose definition does for it + # bundled - the image has no pip and brings its dependencies installed, so the volume + # is populated by copying the installed library tree instead + # The volumes get the library directory in their names so that an environment upgraded + # from a legacy image gets fresh volumes: a volume that already holds the old + # site-packages and the old interpreter in /usr/local/bin would otherwise be mounted + # into the new image. + # The legacy directories are the ones the RDM-osf.io docker-compose.yml mounts. + local legacy_wb_libdir='/usr/local/lib/python3.6' + local legacy_mfr_libdir='/usr/local/lib/python3.5' + + local wb_layout="${WB_LAYOUT:-legacy}" + local wb_libdir="${WB_LIBDIR:-}" + local wb_libname='' wb_voltag='' + local wb_requirements_override='' + local wb_volumes_override='' + local wb_volumes_declaration='' + + case "$wb_layout" in + legacy) + if [ -n "$wb_libdir" ] && [ "$wb_libdir" != "$legacy_wb_libdir" ]; then + echo "WaterButler image keeps its libraries in $wb_libdir, but the legacy compose definition mounts $legacy_wb_libdir" >&2 + return 1 + fi + ;; + bundled) + wb_libname=$(basename "$wb_libdir") + if ! printf '%s' "$wb_libname" | grep -Eq '^python3\.[0-9]+$'; then + echo "WB_LIBDIR must name the library directory of a WaterButler image with bundled dependencies (got '$wb_libdir')" >&2 + return 1 + fi + wb_voltag=${wb_libname#python} + wb_voltag=${wb_voltag//./_} + wb_requirements_override=" command: + - /bin/bash + - -c + - rm -Rf /${wb_libname}/* && + cp -Rf -p ${wb_libdir} / + volumes: + - wb_requirements_${wb_voltag}_vol:/${wb_libname} + - wb_requirements_${wb_voltag}_local_bin_vol:/usr/local/bin" + wb_volumes_override=" volumes: + - wb_requirements_${wb_voltag}_vol:${wb_libdir} + - wb_requirements_${wb_voltag}_local_bin_vol:/usr/local/bin" + wb_volumes_declaration=" wb_requirements_${wb_voltag}_vol: + external: false + wb_requirements_${wb_voltag}_local_bin_vol: + external: false" + ;; + *) + echo "Unsupported WaterButler dependency layout: $wb_layout" >&2 + return 1 + ;; + esac + + # MFR is handled the same way. An MFR image with bundled dependencies renders office + # documents only through a Celery worker that hands the conversion to a LibreOffice + # server (unoserver), so both are added as services and the legacy "unoconv" listener + # is not used. The MFR server and its worker exchange files through /tmp, so the + # worker mounts the same volume as the server. + local mfr_layout="${MFR_LAYOUT:-legacy}" + local mfr_libdir="${MFR_LIBDIR:-}" + local mfr_libname='' mfr_voltag='' + local unoserver_image="${UNOSERVER_IMAGE:-}" + local mfr_requirements_override='' + local mfr_volumes_override='' + local mfr_depends_on_override='' + local mfr_volumes_declaration='' + local mfr_env_additions='' + + case "$mfr_layout" in + legacy) + if [ -n "$mfr_libdir" ] && [ "$mfr_libdir" != "$legacy_mfr_libdir" ]; then + echo "MFR image keeps its libraries in $mfr_libdir, but the legacy compose definition mounts $legacy_mfr_libdir" >&2 + return 1 + fi + ;; + bundled) + mfr_libname=$(basename "$mfr_libdir") + if ! printf '%s' "$mfr_libname" | grep -Eq '^python3\.[0-9]+$'; then + echo "MFR_LIBDIR must name the library directory of an MFR image with bundled dependencies (got '$mfr_libdir')" >&2 + return 1 + fi + if [ -z "$unoserver_image" ]; then + echo "UNOSERVER_IMAGE must be set for an MFR image with bundled dependencies" >&2 + return 1 + fi + mfr_voltag=${mfr_libname#python} + mfr_voltag=${mfr_voltag//./_} + mfr_requirements_override=" command: + - /bin/bash + - -c + - rm -Rf /${mfr_libname}/* && + cp -Rf -p ${mfr_libdir} / + volumes: + - mfr_requirements_${mfr_voltag}_vol:/${mfr_libname} + - mfr_requirements_${mfr_voltag}_local_bin_vol:/usr/local/bin" + mfr_volumes_override=" volumes: + - mfr_requirements_${mfr_voltag}_vol:${mfr_libdir} + - mfr_requirements_${mfr_voltag}_local_bin_vol:/usr/local/bin" + mfr_depends_on_override=' depends_on: + mfr_worker: + condition: service_healthy' + mfr_volumes_declaration=" mfr_requirements_${mfr_voltag}_vol: + external: false + mfr_requirements_${mfr_voltag}_local_bin_vol: + external: false" + mfr_env_additions=' +# Celery worker and LibreOffice conversion server for office document rendering +TASKS_CONFIG_BROKER_URL=amqp://guest:guest@rabbitmq:5672// +TASKS_CONFIG_WAIT_TIMEOUT=120 +UNOSERVER_EXTENSION_CONFIG_HOST=unoserver +UNOSERVER_EXTENSION_CONFIG_PORT=2003' + ;; + *) + echo "Unsupported MFR dependency layout: $mfr_layout" >&2 + return 1 + ;; + esac + # Use environment variables for images local osf_image="${OSF_IMAGE:-niicloudoperation/rdm-osf.io:latest}" local ember_image="${EMBER_IMAGE:-niicloudoperation/rdm-ember-osf-web:latest}" @@ -234,12 +367,49 @@ create_docker_override() { local elasticsearch_image="${ELASTICSEARCH_IMAGE:-elasticsearch:2}" local y_websocket_url="${Y_WEBSOCKET_URL:-}" + # Services that only exist for an MFR image with bundled dependencies. They are appended + # inside the "services:" block of the override, so the text starts at service indentation. + local mfr_worker_services='' + if [ "$mfr_layout" = "bundled" ]; then + mfr_worker_services=" mfr_worker: + image: ${mfr_image} + command: python3 -m invoke celery --hostname=mfr@%h --concurrency=2 + restart: unless-stopped + env_file: + - .docker-compose.mfr.env + volumes: + - mfr_requirements_${mfr_voltag}_vol:${mfr_libdir} + - mfr_requirements_${mfr_voltag}_local_bin_vol:/usr/local/bin + - wb_tmp_vol:/tmp + depends_on: + rabbitmq: + condition: service_started + unoserver: + condition: service_healthy + healthcheck: + test: [\"CMD-SHELL\", \"python3 -m celery -A mfr.tasks.app:app inspect ping --destination=mfr@\$\$HOSTNAME --timeout=5\"] + interval: 10s + timeout: 10s + retries: 12 + unoserver: + image: ${unoserver_image} + restart: unless-stopped + healthcheck: + test: [\"CMD\", \"python3\", \"-c\", \"from xmlrpc.client import ServerProxy; ServerProxy('http://127.0.0.1:2003').info()\"] + interval: 10s + timeout: 10s + retries: 12" + fi + echo "Creating docker-compose override with:" echo " OSF: $osf_image" echo " Ember: $ember_image" echo " CAS: $cas_image" - echo " MFR: $mfr_image" - echo " WaterButler: $wb_image" + echo " MFR: $mfr_image ($mfr_layout dependencies${mfr_libdir:+, libraries in $mfr_libdir})" + if [ -n "$unoserver_image" ]; then + echo " unoserver: $unoserver_image" + fi + echo " WaterButler: $wb_image ($wb_layout dependencies${wb_libdir:+, libraries in $wb_libdir})" echo " Elasticsearch: $elasticsearch_image" if [ -n "$y_websocket_url" ]; then echo " Y-WebSocket: $y_websocket_url" @@ -294,14 +464,21 @@ services: image: ${cas_image} mfr: image: ${mfr_image} +${mfr_volumes_override} +${mfr_depends_on_override} mfr_requirements: image: ${mfr_image} +${mfr_requirements_override} +${mfr_worker_services} wb: image: ${wb_image} +${wb_volumes_override} wb_worker: image: ${wb_image} +${wb_volumes_override} wb_requirements: image: ${wb_image} +${wb_requirements_override} kaken_elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.14.3 environment: @@ -336,6 +513,21 @@ EOL echo "" >> .docker-compose.wb.env echo "SERVER_CONFIG_DOMAIN=http://192.168.168.167:7777" >> .docker-compose.wb.env + # The RDM-osf.io env file enables the MFR local-development rewrite, which points the + # file URLs embedded in rendered previews (PDF, video, ...) at localhost:7777 and drops + # the mode parameter. The E2E browser is logged in on 192.168.168.167, so those + # requests would reach WaterButler without the session cookie and fail with 401. + sed -i.bak 's/^EXTENSION_CONFIG_LOCAL_DEVELOPMENT=.*/EXTENSION_CONFIG_LOCAL_DEVELOPMENT=0/' .docker-compose.mfr.env + rm -f .docker-compose.mfr.env.bak + if ! grep -q '^EXTENSION_CONFIG_LOCAL_DEVELOPMENT=0$' .docker-compose.mfr.env; then + echo "EXTENSION_CONFIG_LOCAL_DEVELOPMENT could not be disabled in .docker-compose.mfr.env" >&2 + return 1 + fi + + if [ -n "$mfr_env_additions" ]; then + printf '%s\n' "$mfr_env_additions" >> .docker-compose.mfr.env + fi + if [ "${MINIO_ENABLED:-false}" = "true" ]; then local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -349,6 +541,18 @@ EOL "${PWD}/docker-compose.override.yml" \ "${script_dir}/../../../weko/nginx/keys/server.crt" fi + + # Top-level volume declarations go last: the steps above append further services + # to the override file, and they must stay under "services:". + if [ -n "$wb_volumes_declaration" ] || [ -n "$mfr_volumes_declaration" ]; then + printf '\nvolumes:\n' >> docker-compose.override.yml + if [ -n "$wb_volumes_declaration" ]; then + printf '%s\n' "$wb_volumes_declaration" >> docker-compose.override.yml + fi + if [ -n "$mfr_volumes_declaration" ]; then + printf '%s\n' "$mfr_volumes_declaration" >> docker-compose.override.yml + fi + fi } # Function to run Django migrations diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index bcf5dff3..6317aefa 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -39,6 +39,18 @@ on: description: 'Custom MFR image (default: niicloudoperation/rdm-modular-file-renderer:latest)' required: false default: '' + mfr_repository: + description: 'RDM-modular-file-renderer repository (default: RCOSDP/RDM-modular-file-renderer)' + required: false + default: '' + mfr_branch: + description: 'RDM-modular-file-renderer branch to build and test against' + required: false + default: '' + unoserver_image: + description: 'unoserver image for an MFR image that bundles its dependencies (default: built from the MFR sources; ignored when mfr_branch is given). How the MFR and WaterButler images carry their dependencies is detected from the images' + required: false + default: '' wb_image: description: 'Custom WaterButler image (default: niicloudoperation/rdm-waterbutler:latest)' required: false @@ -108,7 +120,16 @@ jobs: build-images: runs-on: ubuntu-latest outputs: + wb_image: ${{ steps.wb_params.outputs.image }} wb_custom: ${{ steps.wb_params.outputs.branch != '' }} + wb_layout: ${{ steps.wb_layout.outputs.layout }} + wb_libdir: ${{ steps.wb_layout.outputs.libdir }} + mfr_image: ${{ steps.mfr_params.outputs.image }} + mfr_custom: ${{ steps.mfr_params.outputs.branch != '' }} + mfr_layout: ${{ steps.mfr_layout.outputs.layout }} + mfr_libdir: ${{ steps.mfr_layout.outputs.libdir }} + unoserver_image: ${{ steps.mfr_params.outputs.unoserver_image }} + unoserver_custom: ${{ steps.unoserver_build.outcome == 'success' }} steps: - name: Checkout test repository uses: actions/checkout@v4 @@ -139,10 +160,15 @@ jobs: printf '%s\n' "$PR_BODY" > /tmp/pr_body.txt echo "repository=$(grep -oP '^-\s*WB_REPOSITORY:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true)" >> $GITHUB_OUTPUT echo "branch=$(grep -oP '^-\s*WB_BRANCH:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true)" >> $GITHUB_OUTPUT + WB_IMAGE=$(grep -oP '^-\s*WB_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true) else echo "repository=${{ github.event.inputs.wb_repository }}" >> $GITHUB_OUTPUT echo "branch=${{ github.event.inputs.wb_branch }}" >> $GITHUB_OUTPUT + WB_IMAGE="${{ github.event.inputs.wb_image }}" fi + # The image is resolved here once for both trigger types and passed on as a job + # output, so every E2E job wires the same image the same way + echo "image=${WB_IMAGE:-niicloudoperation/rdm-waterbutler:latest}" >> $GITHUB_OUTPUT - name: Build custom WaterButler image if: steps.wb_params.outputs.branch != '' @@ -168,6 +194,118 @@ jobs: cd .. docker save -o prebuilt-images/rdm-waterbutler-custom.tar rdm-waterbutler:custom + - name: Detect WaterButler dependency layout + id: wb_layout + run: | + # The E2E jobs wire the image by two facts asked from the image itself, so the + # workflow stays valid when the published image moves to a new Python version: + # - the directory that holds the interpreter's library and site-packages, which + # is where the dependency volume is mounted + # - whether the image ships pip: then its dependencies are installed at startup + # with "invoke install" (legacy); otherwise it brings them installed (bundled) + if [ "${{ steps.wb_params.outputs.branch }}" != "" ]; then + IMAGE="rdm-waterbutler:custom" + else + IMAGE="${{ steps.wb_params.outputs.image }}" + docker pull "$IMAGE" + fi + PURELIB=$(docker run --rm --entrypoint python3 "$IMAGE" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') + if [ -z "$PURELIB" ]; then + echo "Could not detect the library directory of $IMAGE" >&2 + exit 1 + fi + LIBDIR=$(dirname "$PURELIB") + if docker run --rm --entrypoint python3 "$IMAGE" -m pip --version >/dev/null 2>&1; then + LAYOUT=legacy + else + LAYOUT=bundled + fi + echo "Detected $LAYOUT dependency layout in $IMAGE (libraries in $LIBDIR)" + echo "layout=$LAYOUT" >> $GITHUB_OUTPUT + echo "libdir=$LIBDIR" >> $GITHUB_OUTPUT + + - name: Parse MFR parameters + id: mfr_params + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + printf '%s\n' "$PR_BODY" > /tmp/pr_body.txt + echo "repository=$(grep -oP '^-\s*MFR_REPOSITORY:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true)" >> $GITHUB_OUTPUT + echo "branch=$(grep -oP '^-\s*MFR_BRANCH:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true)" >> $GITHUB_OUTPUT + echo "unoserver_image=$(grep -oP '^-\s*UNOSERVER_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true)" >> $GITHUB_OUTPUT + MFR_IMAGE=$(grep -oP '^-\s*MFR_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' \r' || true) + else + echo "repository=${{ github.event.inputs.mfr_repository }}" >> $GITHUB_OUTPUT + echo "branch=${{ github.event.inputs.mfr_branch }}" >> $GITHUB_OUTPUT + echo "unoserver_image=${{ github.event.inputs.unoserver_image }}" >> $GITHUB_OUTPUT + MFR_IMAGE="${{ github.event.inputs.mfr_image }}" + fi + echo "image=${MFR_IMAGE:-niicloudoperation/rdm-modular-file-renderer:latest}" >> $GITHUB_OUTPUT + + - name: Build custom MFR images + if: steps.mfr_params.outputs.branch != '' + run: | + MFR_REPO="${{ steps.mfr_params.outputs.repository || 'RCOSDP/RDM-modular-file-renderer' }}" + MFR_BRANCH="${{ steps.mfr_params.outputs.branch }}" + MFR_BASE_BRANCH="develop" + + echo "Building custom MFR images from ${MFR_REPO}@${MFR_BRANCH}" + + # Clone base branch + git clone --depth=50 -b "$MFR_BASE_BRANCH" "https://github.com/${MFR_REPO}.git" RDM-modular-file-renderer + cd RDM-modular-file-renderer + + # Merge custom branch + git fetch origin "$MFR_BRANCH" + git config user.name "github-actions[bot]" + git config user.email "RDM-e2e-test-nb@example.com" + git merge --no-ff FETCH_HEAD -m "Merge ${MFR_BRANCH} for E2E tests" + + # Build the MFR image and the LibreOffice conversion server it renders office documents with + docker build -t rdm-mfr:custom . + docker build -f Dockerfile.unoserver -t rdm-unoserver:custom . + cd .. + docker save -o prebuilt-images/rdm-mfr-custom.tar rdm-mfr:custom rdm-unoserver:custom + + - name: Detect MFR dependency layout + id: mfr_layout + run: | + if [ "${{ steps.mfr_params.outputs.branch }}" != "" ]; then + IMAGE="rdm-mfr:custom" + else + IMAGE="${{ steps.mfr_params.outputs.image }}" + docker pull "$IMAGE" + fi + PURELIB=$(docker run --rm --entrypoint python3 "$IMAGE" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') + if [ -z "$PURELIB" ]; then + echo "Could not detect the library directory of $IMAGE" >&2 + exit 1 + fi + LIBDIR=$(dirname "$PURELIB") + if docker run --rm --entrypoint python3 "$IMAGE" -m pip --version >/dev/null 2>&1; then + LAYOUT=legacy + else + LAYOUT=bundled + fi + echo "Detected $LAYOUT dependency layout in $IMAGE (libraries in $LIBDIR)" + echo "layout=$LAYOUT" >> $GITHUB_OUTPUT + echo "libdir=$LIBDIR" >> $GITHUB_OUTPUT + + - name: Build unoserver image for a published MFR image + id: unoserver_build + if: steps.mfr_layout.outputs.layout == 'bundled' && steps.mfr_params.outputs.branch == '' && steps.mfr_params.outputs.unoserver_image == '' + run: | + MFR_REPO="${{ steps.mfr_params.outputs.repository || 'RCOSDP/RDM-modular-file-renderer' }}" + + # An MFR image with bundled dependencies renders office documents through unoserver, and + # no published unoserver image exists yet, so it is built from the Dockerfile kept with + # the MFR sources + echo "Building unoserver image from ${MFR_REPO}@develop" + git clone --depth=1 -b develop "https://github.com/${MFR_REPO}.git" RDM-modular-file-renderer + docker build -f RDM-modular-file-renderer/Dockerfile.unoserver -t rdm-unoserver:custom RDM-modular-file-renderer + docker save -o prebuilt-images/rdm-unoserver-custom.tar rdm-unoserver:custom + - name: Upload prebuilt images uses: actions/upload-artifact@v4 with: @@ -189,6 +327,7 @@ jobs: skip_admin: true skip_metadata: false skip_autofill: true + mfr_preview_enabled: true minio_enabled: false weko_enabled: false mibyo_db_enabled: false @@ -724,6 +863,17 @@ jobs: docker load -i prebuilt-images/rdm-waterbutler-custom.tar echo "WB_IMAGE=rdm-waterbutler:custom" >> "$GITHUB_ENV" fi + + if [ "${{ needs.build-images.outputs.mfr_custom }}" = "true" ]; then + docker load -i prebuilt-images/rdm-mfr-custom.tar + echo "MFR_IMAGE=rdm-mfr:custom" >> "$GITHUB_ENV" + echo "UNOSERVER_IMAGE=rdm-unoserver:custom" >> "$GITHUB_ENV" + fi + + if [ "${{ needs.build-images.outputs.unoserver_custom }}" = "true" ]; then + docker load -i prebuilt-images/rdm-unoserver-custom.tar + echo "UNOSERVER_IMAGE=rdm-unoserver:custom" >> "$GITHUB_ENV" + fi rm -rf prebuilt-images - name: Install Docker Compose @@ -1012,8 +1162,6 @@ jobs: OSF_OVERRIDE=$(grep -oP '^-\s*OSF_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' ' || true) EMBER_OVERRIDE=$(grep -oP '^-\s*EMBER_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' ' || true) CAS_OVERRIDE=$(grep -oP '^-\s*CAS_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' ' || true) - MFR_OVERRIDE=$(grep -oP '^-\s*MFR_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' ' || true) - WB_OVERRIDE=$(grep -oP '^-\s*WB_IMAGE:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d ' ' || true) EXCLUDE_NOTEBOOKS_OVERRIDE=$(grep -oP '^-\s*EXCLUDE_NOTEBOOKS:\s*\K.*$' /tmp/pr_body.txt | head -1 | tr -d '\r' || true) # Extract TLJH parameters from PR body @@ -1038,12 +1186,6 @@ jobs: fi export EMBER_IMAGE="${EMBER_OVERRIDE:-niicloudoperation/rdm-ember-osf-web:latest}" export CAS_IMAGE="${CAS_OVERRIDE:-niicloudoperation/rdm-cas-overlay:latest}" - export MFR_IMAGE="${MFR_OVERRIDE:-niicloudoperation/rdm-modular-file-renderer:latest}" - if [[ -n "$WB_IMAGE" ]]; then - export WB_IMAGE - else - export WB_IMAGE="${WB_OVERRIDE:-niicloudoperation/rdm-waterbutler:latest}" - fi else # Use workflow dispatch inputs or defaults # If WIKI_ENABLED, OSF_IMAGE was already set to the wiki-built image; preserve it @@ -1054,13 +1196,16 @@ jobs: fi export EMBER_IMAGE="${{ github.event.inputs.ember_image || 'niicloudoperation/rdm-ember-osf-web:latest' }}" export CAS_IMAGE="${{ github.event.inputs.cas_image || 'niicloudoperation/rdm-cas-overlay:latest' }}" - export MFR_IMAGE="${{ github.event.inputs.mfr_image || 'niicloudoperation/rdm-modular-file-renderer:latest' }}" - if [[ -n "$WB_IMAGE" ]]; then - export WB_IMAGE - else - export WB_IMAGE="${{ github.event.inputs.wb_image || 'niicloudoperation/rdm-waterbutler:latest' }}" - fi fi + # MFR and WaterButler were resolved by build-images, which also detected how each + # image carries its dependencies; images built there were already set when loaded + export MFR_IMAGE="${MFR_IMAGE:-${{ needs.build-images.outputs.mfr_image }}}" + export WB_IMAGE="${WB_IMAGE:-${{ needs.build-images.outputs.wb_image }}}" + export MFR_LAYOUT="${{ needs.build-images.outputs.mfr_layout }}" + export MFR_LIBDIR="${{ needs.build-images.outputs.mfr_libdir }}" + export WB_LAYOUT="${{ needs.build-images.outputs.wb_layout }}" + export WB_LIBDIR="${{ needs.build-images.outputs.wb_libdir }}" + export UNOSERVER_IMAGE="${UNOSERVER_IMAGE:-${{ needs.build-images.outputs.unoserver_image }}}" source ${{ github.workspace }}/e2e-tests/.github/scripts/setup_rdm.sh create_docker_override @@ -1101,6 +1246,8 @@ jobs: run: | source ${{ github.workspace }}/e2e-tests/.github/scripts/setup_rdm.sh export INCLUDE_ADMIN=${{ matrix.test-group.include_admin }} + export MFR_LAYOUT="${{ needs.build-images.outputs.mfr_layout }}" + export MFR_PREVIEW_ENABLED=${{ matrix.test-group.mfr_preview_enabled == true }} start_rdm_services - name: Register OAuth scopes @@ -1466,6 +1613,7 @@ jobs: skip_failed_test: true # Continue on failure transition_timeout: 60000 skip_preview_check: true + mfr_preview_enabled: ${{ matrix.test-group.mfr_preview_enabled == true }} skip_130mb_upload: ${{ matrix.test-group.skip_130mb_upload || false }} skip_default_storage: ${{ matrix.test-group.skip_default_storage || false }} skip_metadata: ${{ matrix.test-group.skip_metadata }} diff --git a/run_tests.py b/run_tests.py index dd25b221..a12f95ab 100644 --- a/run_tests.py +++ b/run_tests.py @@ -40,6 +40,7 @@ def __init__(self, config_path, show_disk_usage=False, failed_result_path=None): self.skip_failed_test = True self.transition_timeout = 60000 self.skip_preview_check = False + self.mfr_preview_enabled = False self.skip_130mb_upload = False self.skip_default_storage = False self.skip_metadata = False @@ -257,6 +258,12 @@ def run_login_tests(self): def run_storage_tests(self): """Run storage-related tests.""" print('\n=== Storage Tests ===') + if self.mfr_preview_enabled: + self.result_notebooks.append(self.run_notebook( + 'テスト手順-ストレージ共通-ファイルプレビュー.ipynb', + target_storage_name='NII Storage', + delete_project=True, + )) # Default storage test if not self.skip_default_storage: diff --git a/scripts/mfr_preview.py b/scripts/mfr_preview.py new file mode 100644 index 00000000..841fc1c5 --- /dev/null +++ b/scripts/mfr_preview.py @@ -0,0 +1,94 @@ +"""Shared assertions for the file-preview procedure notebook.""" + +from playwright.async_api import expect + +from scripts import grdm + + +FRAME = 'iframe[src*="/render"]' + + +def viewer(page): + return page.frame_locator(FRAME) + + +async def open_file(page, filename, timeout): + await grdm.get_select_file_title_locator(page, filename).click() + await expect(page.locator(FRAME)).to_be_visible(timeout=timeout) + + +async def file_list(page, storage_name, timeout): + await page.locator('#projectNavFiles a').click() + await expect(page.locator(FRAME)).to_have_count(0, timeout=timeout) + await expect(grdm.get_select_expanded_storage_title_locator( + page, storage_name + )).to_be_visible(timeout=timeout) + + +async def expect_image(page, timeout): + image = viewer(page).locator('#base-image') + await expect(image).to_be_visible(timeout=timeout) + await expect(image).to_have_js_property('complete', True, timeout=timeout) + await expect(image).to_have_js_property('naturalWidth', 640, timeout=timeout) + await expect(image).to_have_js_property('naturalHeight', 480, timeout=timeout) + + +async def expect_pdf_page(page, number, text, timeout): + frame = viewer(page) + await expect(frame.locator('#viewer .page')).to_have_count(2, timeout=timeout) + await expect(frame.locator('#pageNumber')).to_have_value(str(number), timeout=timeout) + pdf_page = frame.locator(f'#pageContainer{number}') + await expect(pdf_page.locator('.textLayer')).to_contain_text(text, timeout=timeout) + await expect(pdf_page.locator('canvas')).to_be_visible(timeout=timeout) + + +async def expect_table(page, rows, timeout): + frame = viewer(page) + await expect(frame.locator('.slick-header-column')).to_have_text( + ['名称', '数量', '日付'], timeout=timeout + ) + grid_rows = frame.locator('#mfrGrid .slick-row') + await expect(grid_rows).to_have_count(len(rows), timeout=timeout) + for index, cells in enumerate(rows): + await expect(grid_rows.nth(index).locator('.slick-cell')).to_have_text( + cells, timeout=timeout + ) + + +async def wait_video(page, condition, timeout): + video = await viewer(page).locator('video').element_handle(timeout=timeout) + frame = await video.owner_frame() + await frame.wait_for_function(condition, arg=video, timeout=timeout) + + +async def observe_pdf_print(page): + sandbox = await page.locator(FRAME).get_attribute('sandbox') + assert sandbox is None or 'allow-modals' in sandbox.split(), 'The preview iframe blocks printing' + # Observe the viewer's real print path without replacing window.print. + await viewer(page).locator('body').evaluate('''body => { + body.dataset.printPages = '0'; + body.dataset.printComplete = 'false'; + window.addEventListener('beforeprint', () => { + body.dataset.printPages = String( + document.querySelectorAll('#printContainer canvas').length); + }, {once: true}); + window.addEventListener('afterprint', () => { + body.dataset.printComplete = 'true'; + }, {once: true}); + }''') + + +async def expect_pdf_print(page, timeout): + frame = viewer(page) + await expect(frame.locator('body')).to_have_attribute('data-print-pages', '2', timeout=timeout) + await expect(frame.locator('body')).to_have_attribute('data-print-complete', 'true', timeout=timeout) + + +async def wait_image_width(page, original, larger, timeout): + image = await viewer(page).locator('.zoomImage').element_handle(timeout=timeout) + frame = await image.owner_frame() + comparison = 'image.width > original' if larger else 'Math.abs(image.width - original) <= 1' + await frame.wait_for_function( + f'([image, original]) => {comparison}', + arg=[image, original], timeout=timeout, + ) diff --git a/testdata/mfr-preview/README.md b/testdata/mfr-preview/README.md new file mode 100644 index 00000000..6720a6da --- /dev/null +++ b/testdata/mfr-preview/README.md @@ -0,0 +1,22 @@ +# ファイルプレビュー試験データ + +[ファイルプレビューのテスト手順](../../テスト手順-ストレージ共通-ファイルプレビュー.ipynb)で使用するダミーデータです。個人情報や外部サービスへの参照を含みません。 + +| ファイル | 内容 | +| --- | --- | +| preview.txt | UTF-8 の日本語・英数字、3行 | +| preview.png / preview.jpg | 640 × 480。赤い長方形、青い円、緑の三角形、英数字 | +| preview.pdf | 2ページ。日本語、英数字、画像。2ページ目だけに検索語「検索確認」を配置 | +| preview.docx | 明示的な改ページを含む2ページ。各ページに日本語、英数字、画像 | +| preview.pptx | 2スライド。各スライドに日本語、英数字、画像 | +| preview.csv | UTF-8。日本語、整数・負の数、日付文字列の2行 | +| preview.xlsx | 「測定値」「集計」の2シート。日付は文字列ではなく Excel の日付型 | +| preview.webm | VP9、320 × 240、10 fps、12秒、音声なしの動くカラーパターン | + +画像の期待結果: + +![図形と文字の配置](preview.png) + +PDF は ReportLab の `UnicodeCIDFont('HeiseiMin-W3')` を使用して作成し、日本語フォントを埋め込んでいません。PDF.js が `UniJIS-UCS2-H` と `Adobe-Japan1-UCS2` の CMap を読み込むため、日本語 CMap 配信の不具合を確認できます。DOCX/PPTX の日本語フォントは変換サービス側で解決されます。 + +XLSX の日付型は MFR で `2026-01-02T00:00:00` のように表示されます。CSV の日付文字列は `2026-01-02` のままです。値と操作ごとの期待結果は Notebook を参照してください。 diff --git a/testdata/mfr-preview/preview.csv b/testdata/mfr-preview/preview.csv new file mode 100644 index 00000000..0340a792 --- /dev/null +++ b/testdata/mfr-preview/preview.csv @@ -0,0 +1,3 @@ +名称,数量,日付 +東京,123,2026-01-02 +大阪,-45,2026-03-04 diff --git a/testdata/mfr-preview/preview.docx b/testdata/mfr-preview/preview.docx new file mode 100644 index 00000000..f269062b Binary files /dev/null and b/testdata/mfr-preview/preview.docx differ diff --git a/testdata/mfr-preview/preview.jpg b/testdata/mfr-preview/preview.jpg new file mode 100644 index 00000000..3475ef35 Binary files /dev/null and b/testdata/mfr-preview/preview.jpg differ diff --git a/testdata/mfr-preview/preview.pdf b/testdata/mfr-preview/preview.pdf new file mode 100644 index 00000000..4df94ba9 Binary files /dev/null and b/testdata/mfr-preview/preview.pdf differ diff --git a/testdata/mfr-preview/preview.png b/testdata/mfr-preview/preview.png new file mode 100644 index 00000000..1469a1c1 Binary files /dev/null and b/testdata/mfr-preview/preview.png differ diff --git a/testdata/mfr-preview/preview.pptx b/testdata/mfr-preview/preview.pptx new file mode 100644 index 00000000..4cad2108 Binary files /dev/null and b/testdata/mfr-preview/preview.pptx differ diff --git a/testdata/mfr-preview/preview.txt b/testdata/mfr-preview/preview.txt new file mode 100644 index 00000000..7af927cc --- /dev/null +++ b/testdata/mfr-preview/preview.txt @@ -0,0 +1,3 @@ +MFR preview test +日本語の表示確認:研究データ +ASCII: ABC xyz 0123456789 diff --git a/testdata/mfr-preview/preview.webm b/testdata/mfr-preview/preview.webm new file mode 100644 index 00000000..0ad14752 Binary files /dev/null and b/testdata/mfr-preview/preview.webm differ diff --git a/testdata/mfr-preview/preview.xlsx b/testdata/mfr-preview/preview.xlsx new file mode 100644 index 00000000..1c028303 Binary files /dev/null and b/testdata/mfr-preview/preview.xlsx differ diff --git "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-\343\202\271\343\203\210\343\203\254\343\203\274\343\202\270\345\205\261\351\200\232-\343\203\225\343\202\241\343\202\244\343\203\253\343\203\227\343\203\254\343\203\223\343\203\245\343\203\274.ipynb" "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-\343\202\271\343\203\210\343\203\254\343\203\274\343\202\270\345\205\261\351\200\232-\343\203\225\343\202\241\343\202\244\343\203\253\343\203\227\343\203\254\343\203\223\343\203\245\343\203\274.ipynb" new file mode 100644 index 00000000..009f0a24 --- /dev/null +++ "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-\343\202\271\343\203\210\343\203\254\343\203\274\343\202\270\345\205\261\351\200\232-\343\203\225\343\202\241\343\202\244\343\203\253\343\203\227\343\203\254\343\203\223\343\203\245\343\203\274.ipynb" @@ -0,0 +1,1787 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "3cb34d02", + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "rdm_url = None\n", + "idp_name_1 = None\n", + "idp_username_1 = None\n", + "idp_password_1 = None\n", + "rdm_project_name = None\n", + "target_storage_name = 'NII Storage'\n", + "delete_project = False\n", + "default_result_path = None\n", + "close_on_fail = False\n", + "transition_timeout = 60000\n", + "render_timeout = 120000" + ] + }, + { + "cell_type": "markdown", + "id": "af8bde97", + "metadata": {}, + "source": [ + "# GakuNinRDM 総合テスト [ストレージ共通-ファイルプレビュー]\n", + "\n", + "- サブシステム名: アドオン\n", + "- ページ/アドオン: ストレージのファイル画面\n", + "- 機能分類: ファイルプレビュー\n", + "- シナリオ名: 形式別の初回表示・操作・再表示\n", + "- 用意するテストデータ: `testdata/mfr-preview/` の9ファイル、接続先URL、既存ユーザーのアカウント\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58924238", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "from getpass import getpass\n", + "from pathlib import Path\n", + "from uuid import uuid4\n", + "import importlib\n", + "import tempfile\n", + "\n", + "if rdm_url is None:\n", + " rdm_url = input('GRDM URL: ')\n", + "if idp_username_1 is None:\n", + " idp_username_1 = input(f'Username for {idp_name_1}: ')\n", + "if idp_password_1 is None and idp_name_1 != 'FakeCAS':\n", + " idp_password_1 = getpass(f'Password for {idp_username_1}@{idp_name_1}: ')\n", + "if rdm_project_name is None:\n", + " rdm_project_name = f'TEST-MFR-{datetime.now():%Y%m%d-%H%M%S}-{uuid4().hex[:8]}'\n", + "if default_result_path is None:\n", + " default_result_path = tempfile.mkdtemp(prefix='mfr-preview-')\n", + "\n", + "fixtures = Path('testdata/mfr-preview').resolve()\n", + "for name in ('preview.txt', 'preview.png', 'preview.jpg', 'preview.pdf',\n", + " 'preview.docx', 'preview.pptx', 'preview.csv', 'preview.xlsx', 'preview.webm'):\n", + " assert (fixtures / name).is_file(), name\n", + "print(f'Project: {rdm_project_name}')\n", + "print(f'Results: {default_result_path}')\n", + "\n", + "import scripts.playwright\n", + "importlib.reload(scripts.playwright)\n", + "from scripts.playwright import init_pw_context, run_pw, finish_pw_context, expect\n", + "from scripts import grdm\n", + "from scripts import mfr_preview as preview\n", + "\n", + "await init_pw_context(close_on_fail=close_on_fail, last_path=default_result_path)" + ] + }, + { + "cell_type": "markdown", + "id": "ed0fe847", + "metadata": {}, + "source": [ + "## 新規プライベートウィンドウで GRDM トップページを開く\n", + "\n", + "設定した URL を開く。同意画面が出た場合は内容を確認して「同意する」を押す。ログイン画面が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbb22ed3", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await page.set_viewport_size({'width': 1440, 'height': 1000})\n", + " await page.goto(rdm_url)\n", + " consent = page.locator('//button[text()=\"同意する\"]')\n", + " if await consent.count():\n", + " await consent.click()\n", + " await grdm.expect_anonymous_toppage(page, idp_name_1, transition_timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "1f804cf8", + "metadata": {}, + "source": [ + "## 指定したアカウントでログインする\n", + "\n", + "設定した IdP を選択し、ユーザー名とパスワードを入力してログインする。プロジェクト一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af4260ba", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.login(page, idp_name_1, idp_username_1, idp_password_1,\n", + " transition_timeout=transition_timeout)\n", + " await grdm.expect_dashboard(page, transition_timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "86f86774", + "metadata": {}, + "source": [ + "## テスト専用の新規プロジェクトを作成する\n", + "\n", + "「新規プロジェクト作成」を押し、上に表示したプロジェクト名を入力して作成する。作成後は「ここに留まる」を選ぶ。同名の既存プロジェクトがある場合は、別の名前で始め直す。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49f9bf58", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await expect(page.locator('[data-test-dashboard-item-title]').filter(\n", + " has_text=rdm_project_name\n", + " )).to_have_count(0)\n", + " await page.locator('[data-test-create-project-modal-button]').click()\n", + " await page.locator('input.project-name').fill(rdm_project_name)\n", + " await page.locator('[data-test-create-project-submit]').click()\n", + " await page.locator('button[data-test-stay-here]').click()\n", + " await expect(page.get_by_text(rdm_project_name, exact=True)).to_be_visible(timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "750feaf0", + "metadata": {}, + "source": [ + "## 作成したプロジェクトを開き、上部の「ファイル」をクリックする\n", + "\n", + "対象ストレージが展開され、空のファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a843b7c3", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await page.get_by_text(rdm_project_name, exact=True).click()\n", + " await expect(page.locator('//a[text()=\"アドオン\"]')).to_be_visible(timeout=transition_timeout)\n", + " await page.locator('#projectNavFiles a').click()\n", + " await expect(grdm.get_select_expanded_storage_title_locator(\n", + " page, target_storage_name\n", + " )).to_be_visible(timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "822c8452", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.txt をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.txt` を選択する。アップロードが完了し、一覧に `preview.txt` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c876bf9", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.txt'))\n", + " await grdm.wait_for_uploaded(page, 'preview.txt', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "89b88d32", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.png をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.png` を選択する。アップロードが完了し、一覧に `preview.png` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62939456", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.png'))\n", + " await grdm.wait_for_uploaded(page, 'preview.png', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "be6d8cbf", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.jpg をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.jpg` を選択する。アップロードが完了し、一覧に `preview.jpg` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8f23430", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.jpg'))\n", + " await grdm.wait_for_uploaded(page, 'preview.jpg', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "f6aefdca", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.pdf をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.pdf` を選択する。アップロードが完了し、一覧に `preview.pdf` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eea878d9", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.pdf'))\n", + " await grdm.wait_for_uploaded(page, 'preview.pdf', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "22bc0cd7", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.docx をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.docx` を選択する。アップロードが完了し、一覧に `preview.docx` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ed710d3", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.docx'))\n", + " await grdm.wait_for_uploaded(page, 'preview.docx', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "9e24e9fb", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.pptx をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.pptx` を選択する。アップロードが完了し、一覧に `preview.pptx` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c64f67fe", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.pptx'))\n", + " await grdm.wait_for_uploaded(page, 'preview.pptx', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "12de2eb5", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.csv をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.csv` を選択する。アップロードが完了し、一覧に `preview.csv` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6b5ed66", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.csv'))\n", + " await grdm.wait_for_uploaded(page, 'preview.csv', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "5a82efa1", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.xlsx をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.xlsx` を選択する。アップロードが完了し、一覧に `preview.xlsx` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0724bb6a", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.xlsx'))\n", + " await grdm.wait_for_uploaded(page, 'preview.xlsx', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "479c280e", + "metadata": {}, + "source": [ + "## 対象ストレージに preview.webm をアップロードする\n", + "\n", + "ストレージ名をクリックし、「アップロード」で `testdata/mfr-preview/preview.webm` を選択する。アップロードが完了し、一覧に `preview.webm` が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4c24ed1", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await grdm.get_select_storage_title_locator(page, target_storage_name).click()\n", + " await grdm.upload_file(page, str(fixtures / 'preview.webm'))\n", + " await grdm.wait_for_uploaded(page, 'preview.webm', timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "21bbdd5f", + "metadata": {}, + "source": [ + "## 一覧から preview.txt を開く\n", + "\n", + "「MFR preview test」「日本語の表示確認:研究データ」「ASCII: ABC xyz 0123456789」の3行が表示されること。日本語が欠けたり文字化けしたりせず、改行が保たれていること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b0dff1a", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.txt', render_timeout)\n", + " await expect(preview.viewer(page).locator('pre')).to_have_text(\n", + " (fixtures / 'preview.txt').read_text(encoding='utf-8'), timeout=render_timeout\n", + " )\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "2400d4e1", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1e0fed9", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a100200c", + "metadata": {}, + "source": [ + "## 一覧から preview.txt をもう一度開く\n", + "\n", + "「MFR preview test」「日本語の表示確認:研究データ」「ASCII: ABC xyz 0123456789」の3行が表示されること。日本語が欠けたり文字化けしたりせず、改行が保たれていること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1adffc0", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.txt', render_timeout)\n", + " await expect(preview.viewer(page).locator('pre')).to_have_text(\n", + " (fixtures / 'preview.txt').read_text(encoding='utf-8'), timeout=render_timeout\n", + " )\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "b3310ae0", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143862fb", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a34d5448", + "metadata": {}, + "source": [ + "## 一覧から preview.png を開く\n", + "\n", + "左上に赤い長方形、右上に青い円、左下に緑の三角形、右下に「MFR 0123456789」が表示されること。欠けや縦横比の崩れがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6333294a", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.png', render_timeout)\n", + " await preview.expect_image(page, render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a306f53c", + "metadata": {}, + "source": [ + "## preview.png の画像をクリックし、ホイールを上に回して拡大する\n", + "\n", + "画像の中央をクリックして拡大表示を有効にし、画像上でマウスホイールを上に1回回す。図形と文字がさらに大きく表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f6bc2b5", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " global zoom_original_width\n", + " frame = preview.viewer(page)\n", + " await expect(frame.locator('.zoomImage')).to_have_count(1, timeout=render_timeout)\n", + " await frame.locator('#base-image').locator('..').click()\n", + " zoom = frame.locator('.zoomImage')\n", + " await expect(zoom).to_have_css('opacity', '1')\n", + " zoom_original_width = await zoom.evaluate('image => image.width')\n", + " await page.mouse.wheel(0, -100)\n", + " await preview.wait_image_width(page, zoom_original_width, larger=True, timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "f3fe1b46", + "metadata": {}, + "source": [ + "## preview.png の画像上でホイールを下に回して縮小する\n", + "\n", + "画像上でマウスホイールを下に1回回す。図形と文字が拡大前の大きさに戻ること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1c949be", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('.zoomImage').hover()\n", + " await page.mouse.wheel(0, 100)\n", + " await preview.wait_image_width(page, zoom_original_width, larger=False, timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "3992a0a1", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4585585c", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "bb1389e3", + "metadata": {}, + "source": [ + "## 一覧から preview.png をもう一度開く\n", + "\n", + "左上に赤い長方形、右上に青い円、左下に緑の三角形、右下に「MFR 0123456789」が表示されること。欠けや縦横比の崩れがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c9fed2e", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.png', render_timeout)\n", + " await preview.expect_image(page, render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "23a32eb2", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf00dbd8", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "b6e3c822", + "metadata": {}, + "source": [ + "## 一覧から preview.jpg を開く\n", + "\n", + "左上に赤い長方形、右上に青い円、左下に緑の三角形、右下に「MFR 0123456789」が表示されること。欠けや縦横比の崩れがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8512a859", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.jpg', render_timeout)\n", + " await preview.expect_image(page, render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "1867388b", + "metadata": {}, + "source": [ + "## preview.jpg の画像をクリックし、ホイールを上に回して拡大する\n", + "\n", + "画像の中央をクリックして拡大表示を有効にし、画像上でマウスホイールを上に1回回す。図形と文字がさらに大きく表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da7675c2", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " global zoom_original_width\n", + " frame = preview.viewer(page)\n", + " await expect(frame.locator('.zoomImage')).to_have_count(1, timeout=render_timeout)\n", + " await frame.locator('#base-image').locator('..').click()\n", + " zoom = frame.locator('.zoomImage')\n", + " await expect(zoom).to_have_css('opacity', '1')\n", + " zoom_original_width = await zoom.evaluate('image => image.width')\n", + " await page.mouse.wheel(0, -100)\n", + " await preview.wait_image_width(page, zoom_original_width, larger=True, timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "f5385acc", + "metadata": {}, + "source": [ + "## preview.jpg の画像上でホイールを下に回して縮小する\n", + "\n", + "画像上でマウスホイールを下に1回回す。図形と文字が拡大前の大きさに戻ること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70cd1629", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('.zoomImage').hover()\n", + " await page.mouse.wheel(0, 100)\n", + " await preview.wait_image_width(page, zoom_original_width, larger=False, timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "787591b4", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00fde82b", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "505f7f11", + "metadata": {}, + "source": [ + "## 一覧から preview.jpg をもう一度開く\n", + "\n", + "左上に赤い長方形、右上に青い円、左下に緑の三角形、右下に「MFR 0123456789」が表示されること。欠けや縦横比の崩れがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b8e8fdb", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.jpg', render_timeout)\n", + " await preview.expect_image(page, render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "0eb2fdef", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "720735a7", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "6ab75b2a", + "metadata": {}, + "source": [ + "## 一覧から preview.pdf を開く\n", + "\n", + "全2ページのうち1ページ目が表示されること。「日本語プレビュー 1ページ目」「研究データ」「MFR PDF page 1」と、画像と同じ3色の図形・英数字が読めること。日本語が空白や四角になっていないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8edde8d", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.pdf', render_timeout)\n", + " await preview.expect_pdf_page(page, 1, '研究データ', render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "3fc0bdb7", + "metadata": {}, + "source": [ + "## PDF の「次のページ」を押す\n", + "\n", + "ページ番号が2に変わり、「日本語プレビュー 2ページ目」「検索確認」「MFR PDF page 2」と図形が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "138216e6", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('#next').click()\n", + " await preview.expect_pdf_page(page, 2, '検索確認', render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "14af511e", + "metadata": {}, + "source": [ + "## PDF の「前のページ」を押す\n", + "\n", + "全2ページのうち1ページ目が表示されること。「日本語プレビュー 1ページ目」「研究データ」「MFR PDF page 1」と、画像と同じ3色の図形・英数字が読めること。日本語が空白や四角になっていないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c290594", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('#previous').click()\n", + " await preview.expect_pdf_page(page, 1, '研究データ', render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "e93ccc10", + "metadata": {}, + "source": [ + "## PDF の検索欄で「検索確認」を検索する\n", + "\n", + "ツールバーの検索ボタンを押し、「検索確認」を入力して Enter を押す。2ページ目に移動し、一致した文字が強調表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6f1dc64", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " frame = preview.viewer(page)\n", + " await frame.locator('#viewFind').click()\n", + " await frame.locator('#findInput').fill('検索確認')\n", + " await frame.locator('#findInput').press('Enter')\n", + " await preview.expect_pdf_page(page, 2, '検索確認', render_timeout)\n", + " await expect(frame.locator('.textLayer .highlight.selected')).to_have_text('検索確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "3f8639d8", + "metadata": {}, + "source": [ + "## PDF の「印刷」を選ぶ\n", + "\n", + "ツールバーの「印刷」を押す。画面幅が狭く印刷ボタンが隠れている場合は、右端の追加ツールボタン(≫)を開き、「印刷」を押す。印刷ダイアログに2ページが現れ、日本語と図形が含まれることを確認し、キャンセルする。\n", + "\n", + "コードは実際の印刷ボタンを押し、2ページ分の印刷用 canvas が作られ、印刷処理が終了することを確認する。\n", + "コードで使用するヘッドレスブラウザーには印刷ダイアログが出ないため、ダイアログ内のページ数・字形・配置は人が別途確認する。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e907af0", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.observe_pdf_print(page)\n", + " frame = preview.viewer(page)\n", + " if await frame.locator('#print').is_visible():\n", + " await frame.locator('#print').click()\n", + " else:\n", + " await frame.locator('#secondaryToolbarToggle').click()\n", + " await frame.locator('#secondaryPrint').click()\n", + " await preview.expect_pdf_print(page, render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "7901d5ee", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4275cb2", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "d37c0905", + "metadata": {}, + "source": [ + "## 一覧から preview.pdf をもう一度開く\n", + "\n", + "ページ番号欄に `1` を入力して Enter を押す。前回の表示位置にかかわらず、1ページ目を確認する。\n", + "\n", + "全2ページのうち1ページ目が表示されること。「日本語プレビュー 1ページ目」「研究データ」「MFR PDF page 1」と、画像と同じ3色の図形・英数字が読めること。日本語が空白や四角になっていないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d96d4cf", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.pdf', render_timeout)\n", + " await expect(preview.viewer(page).locator('#viewer .page canvas').first).to_be_visible(timeout=render_timeout)\n", + " await preview.viewer(page).locator('#pageNumber').fill('1')\n", + " await preview.viewer(page).locator('#pageNumber').press('Enter')\n", + " await preview.expect_pdf_page(page, 1, '研究データ', render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "55ec8dd4", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "234508cb", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "9bcf0191", + "metadata": {}, + "source": [ + "## 一覧から preview.docx を開き、変換後の1ページ目を確認する\n", + "\n", + "変換が完了すると PDF ビューアーに全2ページが表示されること。1ページ目に「MFR DOCX page 1」「日本語の表示確認 1ページ目」と3色の図形・英数字が表示され、欠けや文字化けがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b7f0518", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.docx', render_timeout)\n", + " await preview.expect_pdf_page(page, 1, 'MFR DOCX page 1', render_timeout)\n", + " await expect(preview.viewer(page).locator('#pageContainer1 .textLayer')).to_contain_text('日本語の表示確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "310e6e37", + "metadata": {}, + "source": [ + "## preview.docx の「次のページ」を押す\n", + "\n", + "2ページ目に「MFR DOCX page 2」「日本語の表示確認 2ページ目」と図形が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a98d98b", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('#next').click()\n", + " await preview.expect_pdf_page(page, 2, 'MFR DOCX page 2', render_timeout)\n", + " await expect(preview.viewer(page).locator('#pageContainer2 .textLayer')).to_contain_text('日本語の表示確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "c0d76938", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9faf8e5", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "3fa7d8f6", + "metadata": {}, + "source": [ + "## 一覧から preview.docx をもう一度開く\n", + "\n", + "ページ番号欄に `1` を入力して Enter を押す。前回の表示位置にかかわらず、1ページ目を確認する。\n", + "\n", + "変換が完了すると PDF ビューアーに全2ページが表示されること。1ページ目に「MFR DOCX page 1」「日本語の表示確認 1ページ目」と3色の図形・英数字が表示され、欠けや文字化けがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "042be534", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.docx', render_timeout)\n", + " await expect(preview.viewer(page).locator('#viewer .page canvas').first).to_be_visible(timeout=render_timeout)\n", + " await preview.viewer(page).locator('#pageNumber').fill('1')\n", + " await preview.viewer(page).locator('#pageNumber').press('Enter')\n", + " await preview.expect_pdf_page(page, 1, 'MFR DOCX page 1', render_timeout)\n", + " await expect(preview.viewer(page).locator('#pageContainer1 .textLayer')).to_contain_text('日本語の表示確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "422c2195", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98ed9770", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "3fdae9ee", + "metadata": {}, + "source": [ + "## 一覧から preview.pptx を開き、変換後の1ページ目を確認する\n", + "\n", + "変換が完了すると PDF ビューアーに全2ページが表示されること。1ページ目に「MFR PPTX page 1」「日本語の表示確認 1ページ目」と3色の図形・英数字が表示され、欠けや文字化けがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38acab0e", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.pptx', render_timeout)\n", + " await preview.expect_pdf_page(page, 1, 'MFR PPTX page 1', render_timeout)\n", + " await expect(preview.viewer(page).locator('#pageContainer1 .textLayer')).to_contain_text('日本語の表示確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "16f759e9", + "metadata": {}, + "source": [ + "## preview.pptx の「次のページ」を押す\n", + "\n", + "2ページ目に「MFR PPTX page 2」「日本語の表示確認 2ページ目」と図形が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "566c3867", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('#next').click()\n", + " await preview.expect_pdf_page(page, 2, 'MFR PPTX page 2', render_timeout)\n", + " await expect(preview.viewer(page).locator('#pageContainer2 .textLayer')).to_contain_text('日本語の表示確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "62b8af59", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2330651c", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "200f12ee", + "metadata": {}, + "source": [ + "## 一覧から preview.pptx をもう一度開く\n", + "\n", + "ページ番号欄に `1` を入力して Enter を押す。前回の表示位置にかかわらず、1ページ目を確認する。\n", + "\n", + "変換が完了すると PDF ビューアーに全2ページが表示されること。1ページ目に「MFR PPTX page 1」「日本語の表示確認 1ページ目」と3色の図形・英数字が表示され、欠けや文字化けがないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8756b231", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.pptx', render_timeout)\n", + " await expect(preview.viewer(page).locator('#viewer .page canvas').first).to_be_visible(timeout=render_timeout)\n", + " await preview.viewer(page).locator('#pageNumber').fill('1')\n", + " await preview.viewer(page).locator('#pageNumber').press('Enter')\n", + " await preview.expect_pdf_page(page, 1, 'MFR PPTX page 1', render_timeout)\n", + " await expect(preview.viewer(page).locator('#pageContainer1 .textLayer')).to_contain_text('日本語の表示確認', timeout=render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a02469ff", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "561c4ecc", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "3bcb1663", + "metadata": {}, + "source": [ + "## 一覧から preview.csv を開く\n", + "\n", + "見出しが「名称」「数量」「日付」、データが次の2行であること。日本語・負の数・日付が欠けずに表示されること。\n", + "\n", + "| 名称 | 数量 | 日付 |\n", + "| --- | --- | --- |\n", + "| 東京 | 123 | 2026-01-02 |\n", + "| 大阪 | -45 | 2026-03-04 |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "440dfd52", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.csv', render_timeout)\n", + " await preview.expect_table(page, [\n", + " ['東京', '123', '2026-01-02'], ['大阪', '-45', '2026-03-04'],\n", + " ], render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "b361226d", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cca74003", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "d80c6fe1", + "metadata": {}, + "source": [ + "## 一覧から preview.csv をもう一度開く\n", + "\n", + "見出しが「名称」「数量」「日付」、データが次の2行であること。日本語・負の数・日付が欠けずに表示されること。\n", + "\n", + "| 名称 | 数量 | 日付 |\n", + "| --- | --- | --- |\n", + "| 東京 | 123 | 2026-01-02 |\n", + "| 大阪 | -45 | 2026-03-04 |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06f669a8", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.csv', render_timeout)\n", + " await preview.expect_table(page, [\n", + " ['東京', '123', '2026-01-02'], ['大阪', '-45', '2026-03-04'],\n", + " ], render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "b3892173", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38c295d1", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "1cdfe8bb", + "metadata": {}, + "source": [ + "## 一覧から preview.xlsx を開き、「測定値」を確認する\n", + "\n", + "見出しが「名称」「数量」「日付」、データが次の2行であること。日本語・負の数・日付が欠けずに表示されること。\n", + "\n", + "| 名称 | 数量 | 日付 |\n", + "| --- | --- | --- |\n", + "| 東京 | 123 | 2026-01-02 |\n", + "| 大阪 | -45 | 2026-03-04 |\n", + "\n", + "「測定値」「集計」の2つのシートが表示され、最初は「測定値」が開くこと。日付型のセルは `2026-01-02T00:00:00` のように時刻付きで表示される。Excel の内部シリアル値になっていないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b46f9b95", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.xlsx', render_timeout)\n", + " await expect(preview.viewer(page).locator('#tabular-tabs a[role=tab]')).to_have_text(['測定値', '集計'], timeout=render_timeout)\n", + " await preview.expect_table(page, [\n", + " ['東京', '123', '2026-01-02T00:00:00'],\n", + " ['大阪', '-45', '2026-03-04T00:00:00'],\n", + " ], render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "422887bc", + "metadata": {}, + "source": [ + "## XLSX の「集計」シートをクリックする\n", + "\n", + "表が切り替わり、1行だけ「合計」「78」「2026-03-05T00:00:00」が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d12aaca", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('#tabular-tabs').get_by_role('tab', name='集計', exact=True).click()\n", + " await preview.expect_table(page, [['合計', '78', '2026-03-05T00:00:00']], render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "bfe5f29e", + "metadata": {}, + "source": [ + "## XLSX の「測定値」シートに戻る\n", + "\n", + "見出しが「名称」「数量」「日付」、データが次の2行であること。日本語・負の数・日付が欠けずに表示されること。\n", + "\n", + "| 名称 | 数量 | 日付 |\n", + "| --- | --- | --- |\n", + "| 東京 | 123 | 2026-01-02 |\n", + "| 大阪 | -45 | 2026-03-04 |\n", + "\n", + "「測定値」「集計」の2つのシートが表示され、最初は「測定値」が開くこと。日付型のセルは `2026-01-02T00:00:00` のように時刻付きで表示される。Excel の内部シリアル値になっていないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bc23cea", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('#tabular-tabs').get_by_role('tab', name='測定値', exact=True).click()\n", + " await expect(preview.viewer(page).locator('#tabular-tabs a[role=tab]')).to_have_text(['測定値', '集計'], timeout=render_timeout)\n", + " await preview.expect_table(page, [\n", + " ['東京', '123', '2026-01-02T00:00:00'],\n", + " ['大阪', '-45', '2026-03-04T00:00:00'],\n", + " ], render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a5854a92", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f9f563d", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "7d3c31c4", + "metadata": {}, + "source": [ + "## 一覧から preview.xlsx をもう一度開く\n", + "\n", + "見出しが「名称」「数量」「日付」、データが次の2行であること。日本語・負の数・日付が欠けずに表示されること。\n", + "\n", + "| 名称 | 数量 | 日付 |\n", + "| --- | --- | --- |\n", + "| 東京 | 123 | 2026-01-02 |\n", + "| 大阪 | -45 | 2026-03-04 |\n", + "\n", + "「測定値」「集計」の2つのシートが表示され、最初は「測定値」が開くこと。日付型のセルは `2026-01-02T00:00:00` のように時刻付きで表示される。Excel の内部シリアル値になっていないこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f0d4036", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.xlsx', render_timeout)\n", + " await expect(preview.viewer(page).locator('#tabular-tabs a[role=tab]')).to_have_text(['測定値', '集計'], timeout=render_timeout)\n", + " await preview.expect_table(page, [\n", + " ['東京', '123', '2026-01-02T00:00:00'],\n", + " ['大阪', '-45', '2026-03-04T00:00:00'],\n", + " ], render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "ab86dabc", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9dc8632", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "8eb6f15e", + "metadata": {}, + "source": [ + "## 一覧から preview.webm を開く\n", + "\n", + "動画プレーヤーと再生ボタンが表示され、停止した状態で開くこと。この動画は12秒・音声なしのカラーパターンである。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "528dc01e", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.webm', render_timeout)\n", + " await expect(preview.viewer(page).locator('video')).to_be_visible(timeout=render_timeout)\n", + " await expect(preview.viewer(page).locator('video')).to_have_js_property('paused', True)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "23e93b7d", + "metadata": {}, + "source": [ + "## 動画プレーヤーにフォーカスし、Space キーで再生する\n", + "\n", + "Tab キーで動画プレーヤーへフォーカスを移し、Space キーを押す。カラーパターンが動き、再生位置が進むこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84f109c7", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('video').press('Space')\n", + " await preview.wait_video(page, 'video => !video.paused && video.currentTime >= 1 && video.videoWidth === 320', render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "0426c1e3", + "metadata": {}, + "source": [ + "## Space キーで動画を一時停止する\n", + "\n", + "動画プレーヤーにフォーカスしたまま Space キーを押す。映像と再生位置が止まること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bedb5c3a", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('video').press('Space')\n", + " await expect(preview.viewer(page).locator('video')).to_have_js_property('paused', True)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "1977371a", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de528624", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "f3a6dde1", + "metadata": {}, + "source": [ + "## 一覧から preview.webm をもう一度開く\n", + "\n", + "動画プレーヤーと再生ボタンが表示され、停止した状態で開くこと。この動画は12秒・音声なしのカラーパターンである。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36518451", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.open_file(page, 'preview.webm', render_timeout)\n", + " await expect(preview.viewer(page).locator('video')).to_be_visible(timeout=render_timeout)\n", + " await expect(preview.viewer(page).locator('video')).to_have_js_property('paused', True)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a93bae7a", + "metadata": {}, + "source": [ + "## 再表示した動画を Space キーで再生する\n", + "\n", + "再表示後もカラーパターンが動き、再生位置が進むこと。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61877718", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.viewer(page).locator('video').press('Space')\n", + " await preview.wait_video(page, 'video => !video.paused && video.currentTime >= 1 && video.videoWidth === 320', render_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "b781f48a", + "metadata": {}, + "source": [ + "## 上部の「ファイル」をクリックして一覧に戻る\n", + "\n", + "プレビューが閉じ、対象ストレージのファイル一覧が表示されること。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eaab3d9", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " await preview.file_list(page, target_storage_name, transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "a4d70081", + "metadata": {}, + "source": [ + "## 目視確認を終えたらテスト用プロジェクトを片付ける\n", + "\n", + "`delete_project=True` の場合は、この手順で作成したプロジェクトの「設定」から「プロジェクトの削除」を選び、確認文字列を入力して削除する。プロジェクトを削除した画面が表示されること。\n", + "`False` の場合はプロジェクトを残す。残したプロジェクトは、印刷や描画の目視確認後に同じ操作で削除できる。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a32fb15", + "metadata": {}, + "outputs": [], + "source": [ + "async def _step(page):\n", + " if delete_project:\n", + " await grdm.delete_project(page, transition_timeout=transition_timeout)\n", + " await expect(page.locator('#projectNavFiles a')).to_have_count(0, timeout=transition_timeout)\n", + "\n", + "await run_pw(_step)" + ] + }, + { + "cell_type": "markdown", + "id": "c88d43d5", + "metadata": {}, + "source": [ + "## ブラウザーを閉じて証跡を保存する\n", + "\n", + "自動実行ではブラウザーを閉じ、`default_result_path` に動画・通信記録などを保存する。手動実施では、目視結果を記録してブラウザーを閉じる。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c6314f0", + "metadata": {}, + "outputs": [], + "source": [ + "await finish_pw_context()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}