3.2. Aktionen (Actions)

Überblick

PDF-Dokumente werden durch Aktionen lebendig, interaktiv aber auch komplizierter. Und kompliziert bedeutet, dass sie getestet werden müssen, zumal wenn interaktive Dokumente Teil einer Prozesskette sind, in der Aktionen die Teile sind, die richtig funktionieren müssen.

Eine Aktion ist ein Dictionary-Objekt mit u.a. den Elementen /S und /Type. Das Element /Type hat immer immer den Wert Action und das Element /S (Subtype) hat typabhängige Werte:

// Types of actions:

GoTo:        Set the focus to a destination in the current PDF document
GoToR:       Set the focus to a destination in another PDF document
GoToE:       Go to a destination inside an embedded file
GoTo3DView:  Set the view to a 3D annotation
Hide:        Set the hidden flag of the specified annotation
ImportData:  Import data from a file to the current document
JavaScript:  Execute JavaScript code
Movie:       Play a specified movie
Named:       Execute an action, which is predefined by the PDF viewer
Rendition:   Control the playing of multimedia content
ResetForm:   Set the values of form fields to default
SetOCGState: Set the state of an OCG
Sound:       Play a specified sound
SubmitForm:  Send the form data to an URL
Launch:      Execute an application
Thread:      Set the viewer to the beginning of a specified article
Trans:       Update the display of a document, using a transition dictionary
URI:         Go to the remote URI

Für einige dieser Aktionen stellt PDFUnit Tags zur Verfügung:

<!-- Tags to test actions: -->

<hasNumberOfActions />
<hasNumberOfJavaScriptActions />

<hasAnyAction> 
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasAnyAction>

...  continued
... continuation:

<hasChainedAction>
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasChainedAction>

<hasCloseAction> 
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasCloseAction>

<hasImportDataAction> 
  <matchingComplete    filename=".." />  (tag optional, attribute required)
</hasImportDataAction>

<hasJavaScriptAction> 
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasJavaScriptAction>

<hasLaunchAction> 
  <toLaunch />                           (optional)
</hasLaunchAction>

...  continue
... continuation

<hasLocalGotoAction> 
  <toDestination />                      (optional)
</hasLocalGotoAction>

<hasNamedAction>
  <withName />                           (optional)
</hasNamedAction>

<hasOpenAction>
  <containing        />                  (all nested tags ...
  <matchingComplete  />                  ...
  <matchingRegex     />                  ...
  <withDestinationTo />                  ... are optional) 
</hasOpenAction>

<hasPrintAction>
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasPrintAction>

...  continued
... continuation:

<hasRemoteGotoActionTo file=".."         (required)
                       destination=".."  (optional) 
                       page=".."         (optional)
/>

<hasResetFormAction /> (no attributes and no nested tags!)

<hasSaveAction>
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasSaveAction>

<hasSubmitFormAction> 
  <withDestination />                    (optional)
</hasSubmitFormAction>

<hasURIAction>
  <containing       />                   (all nested tags ...
  <matchingComplete />                   ...
  <matchingRegex    />                   ... are optional) 
</hasURIAction>

...  (end of list)

Die folgenden Abschnitte zeigen Beispiele für verschiedene Aktionen.

Close-Actions

Close-Actions werden beim Schließen eines PDF-Dokumentes ausgeführt:

<!-- The fundamental way to compare actions with expected values: -->

<testcase name="Principle_ComparingActionValues">
  <assertThat testDocument="actions/documentCloseAction.pdf">
    <hasCloseAction>
      <containing>app.alert('A sample for a DOCUMENT_CLOSE-action');</containing>
    </hasCloseAction>
  </assertThat>
</testcase>

Der Inhalt einer Close-Action kann auch mit dem Inhalt einer Datei verglichen werden:

<testcase name="hasCloseAction_MatchingComplete_ContentFromFile">
  <assertThat testDocument="actions/documentCloseAction.pdf">
    <hasCloseAction>
      <matchingComplete filename="actions/documentCloseAction.js" 
                        whitespaces="IGNORE"
      />
    </hasCloseAction>
  </assertThat>
</testcase>

Das Attribut whitespaces=".." ist optional. Die Standard-Whitespace-Behandlung ist NORMALIZE.

ImportData-Actions

ImportData-Actions importieren Daten aus einer Datei. Sie benötigen den Dateinamen als Zusatzinformation:

<testcase name="hasImportDataAction_MatchingFilename">
  <assertThat testDocument="actions/chainedActions.pdf">
    <hasImportDataAction>
      <matchingComplete filename="build.xml" />
    </hasImportDataAction>
  </assertThat>
</testcase>

Es wird nur geprüft, ob die Aktion den erwarteten Dateiname enthält. Ob die Datei selber existiert, wird nicht überprüft.

JavaScript-Actions

Da JavaScript-Text gewöhnlich etwas umfangreicher ist, macht es Sinn, den Vergleichstext für eine JavaScript-Action aus einer Datei zu lesen:

<testcase name="hasJavaScriptAction_MatchingComplete_ContentFromFile">
  <assertThat testDocument="javascript/bookmarkWithJavaScriptAction_OneSimpleAlert.pdf">
    <hasJavaScriptAction>
      <matchingComplete filename="javascript/javascriptAction_OneSimpleAlert.js" />
    </hasJavaScriptAction>
  </assertThat>
</testcase>

Der vollständige Inhalt der JavaScript-Datei wird mit dem Inhalt der JavaScript-Action verglichen. Whitespaces werden dabei normalisiert.

Launch-Actions

Launch-Actions starten Anwendungen oder Skripte. Das kann getestet werden:

<testcase name="hasLaunchAction_Notepad_Print">
  <assertThat testDocument="actions/launchActionToFile.pdf">
    <hasLaunchAction>
      <toLaunch application="c:/windows/notepad.exe" operation="print" />
    </hasLaunchAction>
  </assertThat>
</testcase>

Während des Tests werden lediglich die Inhalte der Attribute application=".." und operation=".." mit denen der Launch-Action des PDF-Dokumentes verglichen. Es wird nicht geprüft, ob die Applikation gestartet werden kann.

Named-Actions

Named-Actions sollten auf ihren Operationsnamen hin überprüft werden:

<testcase name="hasNamedAction_WithName_NextPage">
  <assertThat testDocument="actions/namedActionsNextPages.pdf">
    <hasNamedAction>
      <withName>
        <matchingComplete>/NextPage</matchingComplete>
      </withName>
    </hasNamedAction>
  </assertThat>
</testcase>

Goto-Actions

Goto-Actions benötigen ein Sprungziel in derselben Datei:

<testcase name="hasGotoAction_ToNamedDestination">
  <assertThat testDocument="actions/bookmarksWithPdfOutline.pdf">
    <hasLocalGotoAction>
      <toDestination name="destination2.1" />
    </hasLocalGotoAction>
  </assertThat>
</testcase>

Der Test ist erfolgreich, wenn das aktuelle Test-PDF das erwartete Sprungziel destination2.1 enthält.

GotoRemote-Actions

GotoRemote-Actions benötigen ein Sprungziel in einer anderen Datei:

<testcase name="hasGotoRemoteActionTo_NamedDestination">
  <assertThat testDocument="actions/gotoRemotePageAction.pdf">
    <hasRemoteGotoActionTo file="destination.pdf"
                           destination="destination-3"
    />
  </assertThat>
</testcase>

<testcase name="hasGotoRemoteAction_ToPage">
  <assertThat testDocument="actions/gotoRemotePageAction.pdf">
    <hasRemoteGotoActionTo file="destination.pdf" 
                           page="4" 
    />
  </assertThat>
</testcase>

Es wird lediglich überprüft, ob das Test-PDF-Dokument eine Aktion mit diesem Sprungziel besitzt. Es wird nicht geprüft, ob die Zieldatei bzw. das Sprungziel in der Zieldatei existiert.

Open-Actions

Open-Actions werden beim Laden eines PDF-Dokumentes ausgeführt. Häufig sind es JavaScript- oder Goto-Actions.

<testcase name="hasOpenAction_MultipleInvocation">
  <assertThat testDocument="actions/documentOpenAction_Print.pdf">
    <hasOpenAction>
      <matchingRegex>(?ms).*print(.*)</matchingRegex>
      <matchingComplete>this.print(true);</matchingComplete>
    </hasOpenAction>
  </assertThat>
</testcase>

Innerhalb des Tags <hasOpenAction /> können Texte mit den üblichen Textvergleichs-Tags verglichen werden. Darüber hinaus gibt es für Open-Actions noch das Tag <withDestinationTo />, mit dem das Ziel der Open-Action geprüft wird:

<testcase name="hasOpenAction_GotoPage2">
  <assertThat testDocument="actions/documentOpenAction_Goto.pdf">
    <hasOpenAction>
      <withDestinationTo page="2" />
    </hasOpenAction>
  </assertThat>
</testcase>

Print-Actions

Print-Actions sind JavaScript-Actions, die direkt vor oder direkt nach dem Drucken ausgeführt werden. Sie sind innerhalb von PDF mit den Events WILL_PRINT oder DID_PRINT verbunden.

<testcase name="hasPrintAction_WillPrint">
  <assertThat testDocument="actions/documentPrintActions.pdf">
    <hasPrintAction>
      <matchingComplete>app.alert('A sample for a WILL_PRINT-action');</matchingComplete>
    </hasPrintAction>
  </assertThat>
</testcase>

Analog zu JavaScript-Actions wird der erwartete Inhalt der Print-Action nach einer Normalisierung der Whitespaces mit der erwarteten Zeichenkette verglichen.

ResetForm-Actions

ResetForm-Actions sind parameterlos, ein Textvergleich findet nicht statt. Es wird nur geprüft, ob eine solche Action existiert:

<testcase name="hasResetFormAction">
  <assertThat testDocument="acrofields/javaScriptForFields.pdf">
    <hasResetFormAction />
  </assertThat>
</testcase>

Save-Actions

Save-Actions sind JavaScript-Actions, die direkt vor oder direkt nach dem Speichern ausgeführt werden. Sie sind mit den PDF-Events WILL_SAVE bzw. DID_SAVE verknüpft.

<testcase name="hasSaveAction_MultipleInvocation">
  <assertThat testDocument="actions/documentSaveActions.pdf">
    <hasSaveAction>
      <matchingComplete>app.alert('A sample for a DID_SAVE-action');</matchingComplete>
    </hasSaveAction>
  </assertThat>
</testcase>

Auch hier wird der Vergleich erst nach einer Normalisierung der Whitespaces durchgeführt. Alle üblichen Tags zum Vergleichen von Texten stehen zur Verfügung.

SubmitForm-Actions

SubmitForm-Actions benötigen ein Ziel, an das das Formular geschickt werden soll:

<testcase name="hasSubmitFormAction_ToUri">
  <assertThat testDocument="acrofields/javaScriptForFields.pdf">
    <hasSubmitFormAction>
      <withDestination toURI="http://www.geek-tutorials.com/java/itext/submit.php" /> 
    </hasSubmitFormAction>
  </assertThat>
</testcase>

PDFUnit prüft nicht die Existenz des Ziels, sondern nur die Gleichheit des Sprungziels mit der erwarteten Zeichenkette.

URI-Actions

URI-Actions benötigen eine Ziel-URI:

<testcase name="hasURIAction">
  <assertThat testDocument="actions/noBookmarks-manyActions.pdf">
    <hasURIAction>
      <matchingComplete>http://www.imdb.com/</matchingComplete>
    </hasURIAction>
  </assertThat>
</testcase>

Es findet kein Zugriff auf das Web statt, somit überprüft PDFUnit auch nicht, ob die URI existiert. Es wird nur geprüft, ob das Test-PDF eine URI-Aktion mit diesem Namen enthält.

Any Action - beliebige Aktionen

Das folgende Beispiel zeigt mehrere Testfunktionen, angewendet auf ein Test-PDF, das die verschiedenen Aktionen auch enthält:

<testcase name="hasAnyAction_DifferentKindOfActions">
  <assertThat testDocument="actions/chainedActions.pdf">
    <hasAnyAction>
      <matchingComplete>app.alert('Demo: the first action of five.');</matchingComplete>
      <matchingComplete>http://www.google.de</matchingComplete>
      <matchingComplete>c:/windows/notepad.exe</matchingComplete>
      <matchingComplete>this.print(true);</matchingComplete>
    </hasAnyAction>
  </assertThat>
</testcase>

Whitespace-Behandlung in Vergleichen

Bei Textvergleichen kann die Behandlung der Whitespaces durch den Test gesteuert werden. Im folgenden Beispiel werden Zeilenumbrüche und Leerzeilen ignoriert:

<testcase name="hasCloseAction_Containing_ContentFromReader">
  <assertThat testDocument="actions/documentCloseAction.pdf">
    <hasCloseAction>
      <containing filename="actions/documentCloseAction.js"  whitespaces="IGNORE" />
    </hasCloseAction>
  </assertThat>
</testcase>

Das Kapitel 13.4: „Behandlung von Whitespaces“ geht ausführlich auf den flexiblen Umgang mit Whitespaces ein.