Home / Demo
Student forms
Three Forms modules on a course and student schema: QAFE_FORM1 (courses), QAFE_FORM2 (instructors with their courses, sections and students in four windows, a list of values) and QAFE_FORM3 (courses with counts, sections in a second window, validation triggers).
Target: .NET 10 and PostgreSQL, the forms' windows, blocks, list of values and alerts in the browser. Original Forms source:
github.com/madilraza/oracle-forms-examples
at commit 6b10ffc, licence: Apache-2.0.
The live demos share one database per app: visitors see each other's changes, and the data resets to the scenario fixture every night.
Parity: 15 of 15 scenarios
Each scenario was recorded on the original and replayed against the migrated app. Three runs from a fresh database gave byte-identical diffs for every scenario; none has a defect. Open a scenario for its steps, screenshots of both apps, the golden record, the diff and its waivers.
- Match: the same value. Tolerated: different only within a written rule of the tolerance policy, here the audit dates a save stamps with today's date, and list-of-values rows the Forms client had not fetched yet.
- Waived: 1 waivers are proposed for the customer's decision; 1 of them cover 5 values here, where the migrated app deliberately does not copy the original (see waivers).
- Defect: a difference to repair. Unmapped: none; every recorded value has a counterpart in the migrated app.
| Scenario | What it does | match | tolerated | waived | defect | unmapped | Three runs |
|---|---|---|---|---|---|---|---|
| form1-browse | Browse the courses of QAFE_FORM1: WHEN-NEW-FORM-INSTANCE sets :GLOBAL.USER_ID and queries COURSE; the smartbar's Next Record and Previous Record move through the single-record block. | 59 | 0 | 3 | 0 | 0 | identical |
| form1-insert | Insert a course: PRE-INSERT takes COURSE_NO from COURSE_NO_SEQ and sets CREATED_DATE and MODIFIED_DATE to SYSDATE (the run date, masked) and the user columns to :GLOBAL.USER_ID ('Qafe'). | 68 | 2 | 1 | 0 | 0 | identical |
| form1-update | Change the first course's description and save: PRE-UPDATE sets MODIFIED_DATE to SYSDATE and MODIFIED_BY to :GLOBAL.USER_ID. | 50 | 1 | 0 | 0 | 0 | identical |
| form2-course-delete-refused | Remove Record on a course. FORM2's COURSE block has Delete Allowed No, so Forms refuses with FRM-41049 before the master-detail check (ON-CHECK-DELETE-MASTER) could run. | 360 | 0 | 0 | 0 | 0 | identical |
| form2-courses | Show Courses queries COURSE and shows the Courses window; choosing another course coordinates its sections (ON-POPULATE-DETAILS, QUERY_MASTER_DETAILS, CHECK_PACKAGE_FAILURE); close hides the window and returns to the instructors. | 580 | 0 | 0 | 0 | 0 | identical |
| form2-instructor-insert | Insert an instructor: PRE-INSERT takes INSTRUCTOR_ID from INSTRUCTOR_ID_SEQ and sets the created and modified columns (SYSDATE, :GLOBAL.USER_ID = 'QAFE'). | 382 | 0 | 0 | 0 | 0 | identical |
| form2-student-insert | Insert a student in FORM2. STUDENT_ID is the block's primary key, Required, on no canvas and set by no trigger: the save stops at FRM-40600 (Record has already been inserted) from Forms' primary-key check, so the insert never reaches the database and TRIGGER_STUDENT_BEFORE_INSERT does not run. | 461 | 0 | 0 | 0 | 0 | identical |
| form2-student-no-first-name | Empty a student's first name and save. The form has no check; the database trigger TRIGGER_STUDENT_BEFORE_UPDATE raises ORA-20101 and Forms reports that it could not update the record. | 460 | 0 | 0 | 0 | 0 | identical |
| form2-student-update | Change a student's employer in the Students window and save; the database trigger on STUDENT checks the first name on the update (ORA-20101 when it is empty; not here). | 371 | 0 | 0 | 0 | 0 | identical |
| form2-students | Show Students queries STUDENT into the Students window; Close hides it and returns to the instructors. | 220 | 0 | 0 | 0 | 0 | identical |
| form2-zip-lov | Double-click the instructor's ZIP: WHEN-MOUSE-DOUBLECLICK runs DO_KEY('LIST_VALUES') and the Zip Codes LOV opens; Escape closes it without a choice. The item's LOV is then used through Edit > Display List to choose another zip, and the change is saved (PRE-UPDATE). | 463 | 1 | 0 | 0 | 0 | identical |
| form3-browse | QAFE_FORM3 queries COURSE; POST-QUERY counts each course's sections and enrolled students. Hide Student Count hides the count and a second press shows it again. | 292 | 0 | 1 | 0 | 0 | identical |
| form3-cost-invalid | A course cost below 5 fails WHEN-VALIDATE-ITEM on COST: "Course cost has to be greater than 5". A valid cost then passes. | 365 | 0 | 0 | 0 | 0 | identical |
| form3-section-course | A section's course number is checked by WHEN-VALIDATE-ITEM: an unknown number gives "This course does not exist in the database."; an existing one passes, and the trigger's FETCH INTO :COURSE.COURSE_NO writes it into the master block's course number. | 436 | 0 | 0 | 0 | 0 | identical |
| form3-sections | Show Sections opens the second window; SECTION's WHEN-NEW-BLOCK-INSTANCE queries the course's sections. Close Window runs GO_BACK_TO_HOME_SCREEN. | 168 | 0 | 0 | 0 | 0 | identical |
| total | 4735 | 4 | 5 | 0 | 0 |
The same app with an Angular front end: 15 of 15
The check does not depend on what the new interface is built with. This app has a second front end written in Angular on the same back end and API, and the same scenarios, mapping, tolerance policy and waivers check it: 15 of 15 scenarios at parity, 0 defects, three runs from a fresh database byte-identical, and every diff byte-identical to the default front end's.
| Scenario | match | tolerated | waived | defect | unmapped | Three runs | Diff as the default front end |
|---|---|---|---|---|---|---|---|
| form1-browse | 59 | 0 | 3 | 0 | 0 | identical | identical |
| form1-insert | 68 | 2 | 1 | 0 | 0 | identical | identical |
| form1-update | 50 | 1 | 0 | 0 | 0 | identical | identical |
| form2-course-delete-refused | 360 | 0 | 0 | 0 | 0 | identical | identical |
| form2-courses | 580 | 0 | 0 | 0 | 0 | identical | identical |
| form2-instructor-insert | 382 | 0 | 0 | 0 | 0 | identical | identical |
| form2-student-insert | 461 | 0 | 0 | 0 | 0 | identical | identical |
| form2-student-no-first-name | 460 | 0 | 0 | 0 | 0 | identical | identical |
| form2-student-update | 371 | 0 | 0 | 0 | 0 | identical | identical |
| form2-students | 220 | 0 | 0 | 0 | 0 | identical | identical |
| form2-zip-lov | 463 | 1 | 0 | 0 | 0 | identical | identical |
| form3-browse | 292 | 0 | 1 | 0 | 0 | identical | identical |
| form3-cost-invalid | 365 | 0 | 0 | 0 | 0 | identical | identical |
| form3-section-course | 436 | 0 | 0 | 0 | 0 | identical | identical |
| form3-sections | 168 | 0 | 0 | 0 | 0 | identical | identical |
The app is also rebuilt from native APEX components on its original Oracle schema, checked with the same scenarios: the APEX version.
Faithful to the original
The migrated app copies the original's behaviour, including what looks like a bug. These are the places a visitor will notice.
The interface is new; the behaviour is the original's, and the scenarios check it value by value. Some of that behaviour looks like a bug on a web page. It is kept on purpose, because the point of the demo is that nothing changed:
- Adding a student never saves. In the Students window of QAFE_FORM2, Save stops at
FRM-40600: Record has already been inserted.The block's primary key, STUDENT_ID, is required but on no canvas and set by no trigger, so Forms' own key check refuses every new row before it reaches the database. - Emptying a student's first name fails on Save with
FRM-40509: ORACLE error: unable to UPDATE record.The form has no check; a database trigger on STUDENT raises the error, as in the original schema. - Courses cannot be deleted in QAFE_FORM2: Remove Record answers
FRM-41049: You cannot delete this record., because the block does not allow deletes. - A section's course number overwrites the course. In QAFE_FORM3, typing an existing course number into a section copies it into the course shown above (the original's validation trigger fetches into the master block's item). An unknown number gives "This course does not exist in the database."
- A course cost below 5 is refused with "Course cost has to be greater than 5".
- Lists come in storage order, not sorted. No block has an ORDER BY, so courses, instructors and students appear in the order the database stores them: the student list starts with Yvonne Allende (student 385), as in the original database.
- Audit dates have no time of day. The forms assign SYSDATE to date items, which drops the time, so created and modified dates are stored as midnight.
- Oracle message texts on a PostgreSQL app.
FRM-40400: Transaction complete,FRM-40350: Query caused no records to be retrieved,FRM-40100: At first recordand the other FRM messages are the original's, and the message line is part of the recorded behaviour. Moving the cursor clears it, as in Forms. - Each form runs on its own, as the three original modules do; the start page lists them.
The original's Java client also announces the course number for screen readers when the cursor reaches it ("Course No.454"). That text is not on the screen; the browser leaves announcements to the screen reader (waiver S1).
Coverage of the original
Every trigger and program unit of the Forms modules, measured with Forms Runtime Diagnostics while the
scenarios ran; database PL/SQL measured with DBMS_PLSQL_CODE_COVERAGE.
| Module | Triggers | Program units | Covered | Unreachable | Blocked |
|---|---|---|---|---|---|
| QAFE_FORM1 | 3 | 0 | 3 | 0 | 0 |
| QAFE_FORM2 | 11 | 3 | 13 | 1 | 0 |
| QAFE_FORM3 | 8 | 1 | 9 | 0 | 0 |
| total | 22 | 4 | 25 | 1 | 0 |
Decision branches: 14, of which 10 covered, 4 unreachable (retirement candidates) and 0 blocked. Database PL/SQL: 10 unit(s) in the coverage report, 2 of 3 blocks covered. Exit check: PASS.
Every branch with its class
| Trigger or unit | Branch | Class | Scenarios or reason |
|---|---|---|---|
QAFE_FORM2/COURSE/ON-POPULATE-DETAILS | queried course - sections queried | covered | form2-course-delete-refused, form2-courses |
QAFE_FORM2/COURSE/ON-POPULATE-DETAILS | new course record - nothing | unreachable | FORM2's COURSE block has Insert Allowed No, so it never holds a NEW or INSERT record. |
QAFE_FORM2/unit:QUERY_MASTER_DETAILS | immediate relation - detail queried | covered | form2-course-delete-refused, form2-courses |
QAFE_FORM2/unit:QUERY_MASTER_DETAILS | deferred relation - detail marked non-coordinated | unreachable | COURSE_SECTION, FORM2's only relation, has Deferred No. |
QAFE_FORM2/unit:CLEAR_ALL_MASTER_DETAILS | unchanged details - CLEAR_BLOCK(NO_VALIDATE) | covered | form2-courses |
QAFE_FORM2/unit:CLEAR_ALL_MASTER_DETAILS | changed detail - CLEAR_BLOCK(ASK_COMMIT) | unreachable | The detail block SECTION has Insert, Update and Delete Allowed No (typing gives FRM-40200), so it is never CHANGED. |
QAFE_FORM2/unit:CHECK_PACKAGE_FAILURE | previous built-in succeeded | covered | form2-course-delete-refused, form2-courses |
QAFE_FORM2/unit:CHECK_PACKAGE_FAILURE | previous built-in failed - raise | unreachable | It follows GO_BLOCK and GO_ITEM to SECTION or back to the starting item; FORM2 has no navigation or validation trigger that could make them fail, and SECTION is never changed. |
QAFE_FORM3/COURSE.COST/WHEN-VALIDATE-ITEM | cost below 5 - refused | covered | form3-cost-invalid |
QAFE_FORM3/COURSE.COST/WHEN-VALIDATE-ITEM | cost 5 or more - accepted | covered | form3-cost-invalid |
QAFE_FORM3/SECTION.COURSE_NO/WHEN-VALIDATE-ITEM | unknown course - refused | covered | form3-section-course |
QAFE_FORM3/SECTION.COURSE_NO/WHEN-VALIDATE-ITEM | existing course - accepted | covered | form3-section-course |
QAFE_FORM3/COURSE.HIDESTUDENTCOUNT/WHEN-BUTTON-PRESSED | visible - hidden | covered | form3-browse |
QAFE_FORM3/COURSE.HIDESTUDENTCOUNT/WHEN-BUTTON-PRESSED | hidden - shown | covered | form3-browse |
Database PL/SQL units
| Unit | Type | Blocks | Covered | Class |
|---|---|---|---|---|
| ADF_EX10_01_TRIGGERS | PACKAGE BODY | None | 0 | unreachable |
| STUDENT | PACKAGE BODY | None | 0 | unreachable |
| STUDENTPKG | PACKAGE BODY | None | 0 | unreachable |
| TRIGGER_STUDENT_BEFORE_INSERT | TRIGGER | None | 0 | unreachable |
| TRIGGER_STUDENT_BEFORE_UPDATE | TRIGGER | 3 | 2 | partly covered |
| V_EX01_01_STUDENT_TRG | TRIGGER | None | 0 | unreachable |
| V_EX10_01_STUDENT_TRG | TRIGGER | None | 0 | unreachable |
| V_QAFE_FORM1_COURSE_TRG | TRIGGER | None | 0 | unreachable |
| V_QAFE_FORM2_INSTRUCTOR_TRG | TRIGGER | None | 0 | unreachable |
| V_QAFE_FORM2_STUDENT_TRG | TRIGGER | None | 0 | unreachable |
Waivers
Source behaviour a customer would probably not replicate, proposed for the customer's decision. Each is pinned to the exact values it covers; the counts are values waived in the final diffs. File: waivers.yaml.
Waived: 1 waivers are proposed for the customer's decision; 1 of them cover 5 values here, where the migrated app deliberately does not copy the original (see waivers).
| Id | Waiver | Values | Reason |
|---|---|---|---|
| S1 | Screen-reader announcement of the COURSE_NO item read as a message | 5 | After the cursor enters COURSE.COURSE_NO by record navigation (Next Record, the first record of a query, the Hide Student Count button), the Forms 14.1.2 Java client announces the item for assistive technology: its prompt or hint ("Course No." in QAFE_FORM1, "Course No Required" in QAFE_FORM3) followed by the current value, e.g. "Course No.454". The recorder reads this accessible text where the message line lives, but nothing is written to the visible message line (the screenshots of those steps show an empty status bar) and no form code issues it. A browser target leaves screen-reader announcements to the browser (the item's label is its accessible name), so the visible message line stays empty, as on screen in the source. Only these announcement values are waived; every other message is still compared. |