Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/canvas-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
-only-testing:OpenNotesUITests/NotebookInteractionUITests/testInvalidNativePayloadShowsRecoveryWithoutOverwritingNote \
-collect-test-diagnostics never
- name: Verify persisted lecture content
if: ${{ always() && (steps.interaction_workloads.outcome == 'success' || steps.interaction_workloads.outcome == 'failure') }}
run: |
simulator_id="${IOS_SIMULATOR_DESTINATION##*id=}"
python3 -c 'import subprocess,sys; subprocess.run(["xcrun", "simctl", "bootstatus", sys.argv[1], "-b"], check=True, timeout=180)' "$simulator_id"
Expand All @@ -84,16 +85,24 @@ jobs:
run: |
python3 scripts/ci/collect-canvas-audits.py "$RUNNER_TEMP/canvas-audit-reports"
node scripts/ci/verify-canvas-audit.mjs "$RUNNER_TEMP/canvas-audit-reports"
- name: Profile interactions that missed the budgets
if: ${{ failure() && steps.interaction_workloads.outcome == 'success' && steps.canvas_budgets.outcome == 'failure' }}
- name: Profile failed native workloads
if: ${{ failure() && (steps.interaction_workloads.outcome == 'failure' || (steps.interaction_workloads.outcome == 'success' && steps.canvas_budgets.outcome == 'failure')) }}
run: |
# Restore known geometry before rerunning the edit/relaunch case.
# Timing reports above remain the unprofiled evidence.
simulator_id="${IOS_SIMULATOR_DESTINATION##*id=}"
app_data=$(xcrun simctl get_app_container "$simulator_id" com.builderpro.opennotes data)
mkdir -p "$RUNNER_TEMP/canvas-audit-profiles"
cp -R "$app_data/Documents/notebook-bodies" "$RUNNER_TEMP/canvas-audit-profiles/before-profile-bodies"
python3 scripts/seed-native-audit.py "$simulator_id"
python3 scripts/ci/sample-canvas-process.py OpenNotes "$RUNNER_TEMP/canvas-audit-profiles" &
profile_pid=$!
trap 'kill "$profile_pid" 2>/dev/null || true; wait "$profile_pid" || true' EXIT
xcodebuild test-without-building \
-workspace ios/OpenNotes.xcworkspace -scheme OpenNotesIntegrity \
-derivedDataPath /tmp/opennotes-ui-deriveddata \
-destination "$IOS_SIMULATOR_DESTINATION" -parallel-testing-enabled NO \
-only-testing:OpenNotesUITests/NotebookInteractionUITests/testCrossPageAssetHistory \
-only-testing:OpenNotesUITests/NotebookStallUITests/testDensePagesAndKeyboardInteractionAudit \
-only-testing:OpenNotesUITests/NotebookStallUITests/testDensePanZoomInteractionAudit \
-only-testing:OpenNotesUITests/NotebookStallUITests/testDenseWritingDuringAutosaveInteractionAudit \
Expand Down
2 changes: 2 additions & 0 deletions app/note/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveInkDisplayAppearance } from '@mathnotes/mobile-ink';
import {selectedOverlayTouchRects} from '../../src/utils/overlayTouchRects';
import {useOverlayHistory} from '../../src/components/editor/history/useOverlayHistory';
import React, {
Expand Down Expand Up @@ -940,6 +941,7 @@ export default function NoteScreen() {
style={styles.canvasArea}
>
<InfiniteInkCanvas
displayAppearance={resolveInkDisplayAppearance(theme.isDark ? 'dark' : 'light')}
ref={canvasRef}
style={styles.flex}
initialPageCount={1}
Expand Down
36 changes: 25 additions & 11 deletions ios/OpenNotesUITests/InkVisibilityAssertions.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import XCTest
import UIKit

/// The dense fixture uses dark ink on a light page. Sample only page content,
/// excluding the navigation/toolbars, page edges, and floating AI control.
private func visibleInkSamples(_ image: UIImage) -> Int {
/// Count ink that contrasts with the dominant paper brightness. Sampling page
/// interiors prevents dark paper from falsely satisfying a dark-ink assertion.
func contrastingInkPixels(_ image: UIImage, rect: CGRect? = nil) -> Int {
guard let source = image.cgImage else { return 0 }
let width = source.width, height = source.height
var pixels = [UInt8](repeating: 255, count: width * height * 4)
Expand All @@ -14,11 +14,26 @@ private func visibleInkSamples(_ image: UIImage) -> Int {
else { return 0 }
context.draw(source, in: CGRect(x: 0, y: 0, width: width, height: height))
let data = bytes.bindMemory(to: UInt8.self)
let scale = CGFloat(width) / image.size.width
let interior = CGRect(x: width / 4, y: height / 5, width: width / 2, height: height * 7 / 15)
let area = (rect?.applying(CGAffineTransform(scaleX: scale, y: scale)) ?? interior)
.intersection(CGRect(x: 0, y: 0, width: width, height: height))
guard !area.isNull, !area.isEmpty else { return 0 }
var histogram = [Int](repeating: 0, count: 16)
for y in Int(area.minY)..<Int(area.maxY) {
for x in Int(area.minX)..<Int(area.maxX) {
let i = (y * width + x) * 4
let brightness = (Int(data[i]) + Int(data[i + 1]) + Int(data[i + 2])) / 3
histogram[brightness / 16] += 1
}
}
let paper = histogram.indices.max(by: { histogram[$0] < histogram[$1] }) ?? 15
var count = 0
for y in (height / 5)..<(height * 2 / 3) {
for x in (width / 100)..<(width * 85 / 100) {
let offset = (y * width + x) * 4
if data[offset] < 90 && data[offset + 1] < 90 && data[offset + 2] < 110 { count += 1 }
for y in Int(area.minY)..<Int(area.maxY) {
for x in Int(area.minX)..<Int(area.maxX) {
let i = (y * width + x) * 4
let brightness = (Int(data[i]) + Int(data[i + 1]) + Int(data[i + 2])) / 3
if paper < 8 ? brightness > 160 : brightness < 100 { count += 1 }
}
}
return count
Expand All @@ -27,7 +42,7 @@ private func visibleInkSamples(_ image: UIImage) -> Int {

func assertDenseInkVisible(_ app: XCUIApplication, context: String,
file: StaticString = #filePath, line: UInt = #line) {
let predicate = NSPredicate { _, _ in visibleInkSamples(app.screenshot().image) > 100 }
let predicate = NSPredicate { _, _ in contrastingInkPixels(app.screenshot().image) > 100 }
let result = XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: predicate, object: nil)], timeout: 8)
if result != .completed {
let attachment = XCTAttachment(screenshot: app.screenshot())
Expand All @@ -38,8 +53,7 @@ func assertDenseInkVisible(_ app: XCUIApplication, context: String,
XCTAssertEqual(result, .completed, "Expected visible dense ink: \(context)", file: file, line: line)
}

/// Count newly darkened pixels in a screen-coordinate rectangle, excluding
/// preexisting fixture ink. Both captures use the same transform and viewport.
/// Count changed ink brightness in either appearance at an unchanged viewport.
func changedInkPixels(_ before: UIImage, _ after: UIImage, rect: CGRect) -> Int {
guard let first = before.cgImage, let second = after.cgImage,
first.width == second.width, first.height == second.height else { return 0 }
Expand All @@ -63,7 +77,7 @@ func changedInkPixels(_ before: UIImage, _ after: UIImage, rect: CGRect) -> Int
for y in Int(area.minY)..<Int(area.maxY) {
for x in Int(area.minX)..<Int(area.maxX) {
let i = (y * width + x) * 4
if Int(a[i]) + Int(a[i + 1]) + Int(a[i + 2]) - Int(b[i]) - Int(b[i + 1]) - Int(b[i + 2]) > 100 {
if abs(Int(a[i]) + Int(a[i + 1]) + Int(a[i + 2]) - Int(b[i]) - Int(b[i + 1]) - Int(b[i + 2])) > 100 {
changed += 1
}
}
Expand Down
8 changes: 1 addition & 7 deletions ios/OpenNotesUITests/NotebookInteractionUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,7 @@ final class NotebookInteractionUITests: NotebookUITestCase {
XCTAssertTrue(boundary.waitForExistence(timeout: 10))
let reopenedY = boundary.frame.midY
let reopened = app.screenshot().image
// The lower page fixture is blank. Compare the persisted lower stroke
// against a white reference generated at the same screenshot dimensions.
let white = UIGraphicsImageRenderer(size: reopened.size).image { context in
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: reopened.size))
}
XCTAssertGreaterThan(changedInkPixels(white, reopened,
XCTAssertGreaterThan(contrastingInkPixels(reopened,
rect: CGRect(x: x - 8, y: reopenedY + 25, width: 16, height: 40)), 20,
"Saved crossing stroke must survive full application relaunch")
attach(app, "Cross-page stroke after undo, redo, save and relaunch")
Expand Down
4 changes: 4 additions & 0 deletions ios/OpenNotesUITests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ per page at the editor's 820×1061 point size and 2× native pixel scale.

Keep fixture revisions identical for before/after comparisons. Simulator and
standalone engine measurements do not establish physical-iPad frame rates.

Ink visibility checks contrast against the dominant paper brightness in a page
interior, so dark paper cannot falsely satisfy an ink assertion. Cross-page
change checks accept newly visible ink in either light or dark appearance.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions scripts/ci/sample-canvas-process.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ def stop(_signum, _frame):

signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
deadline = time.monotonic() + 300
# The asset case launches twice, followed by the three stall workloads.
# Keep this diagnostic bounded while covering all of those app lifetimes.
deadline = time.monotonic() + 600
# Ignore an app left open by the preceding timing run.
initial = subprocess.check_output(["ps", "-axo", "pid=,comm="], text=True)
for line in initial.splitlines():
fields = line.strip().split(maxsplit=1)
if len(fields) == 2 and fields[1].endswith(f"/{binary}.app/{binary}"):
seen.add(int(fields[0]))
try:
while not stopping and time.monotonic() < deadline and sampled < 3:
while not stopping and time.monotonic() < deadline and sampled < 6:
listing = subprocess.check_output(["ps", "-axo", "pid=,comm="], text=True)
for line in listing.splitlines():
fields = line.strip().split(maxsplit=1)
Expand Down
3 changes: 2 additions & 1 deletion src/components/editor/FloatingToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveInkDisplayAppearance, inkDisplayColor } from '@mathnotes/mobile-ink';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
LayoutChangeEvent,
Expand Down Expand Up @@ -478,7 +479,7 @@ export function FloatingToolbar({
>
{TOOL_DESCRIPTORS.map((tool) => {
const isActive = activeTool === tool.type;
const tint = tool.supportsColor ? toolColors[tool.type] : undefined;
const tint = tool.supportsColor ? inkDisplayColor(toolColors[tool.type], resolveInkDisplayAppearance(theme.isDark ? 'dark' : 'light')) : undefined;
return (
<View
key={tool.type}
Expand Down
3 changes: 2 additions & 1 deletion src/components/editor/TextBoxOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveInkDisplayAppearance, inkDisplayColor } from '@mathnotes/mobile-ink';
import {useNativeCallback} from '../../hooks/native-callback';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Expand Down Expand Up @@ -348,7 +349,7 @@ export function TextBoxOverlay({
}));

const fontSize = textBox.fontSize ?? 18;
const color = textBox.color || theme.colors.text;
const color = inkDisplayColor(textBox.color || '#000000', resolveInkDisplayAppearance(theme.isDark ? 'dark' : 'light'));
const showChrome = isSelected || isEditing;

return (
Expand Down
3 changes: 2 additions & 1 deletion src/components/editor/ToolSettingsPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveInkDisplayAppearance, inkDisplayColor } from '@mathnotes/mobile-ink';
import React, { useEffect } from 'react';
import {
Modal,
Expand Down Expand Up @@ -166,7 +167,7 @@ export function ToolSettingsPopover({
style={[
styles.swatch,
{
backgroundColor: swatch,
backgroundColor: inkDisplayColor(swatch, resolveInkDisplayAppearance(theme.isDark ? 'dark' : 'light')),
borderColor:
swatch.toLowerCase() === color.toLowerCase()
? theme.colors.accent
Expand Down
Binary file modified vendor/mathnotes-mobile-ink-0.3.10.tgz
Binary file not shown.
6 changes: 3 additions & 3 deletions vendor/mobile-ink-source.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"package": "@mathnotes/mobile-ink",
"version": "0.3.10",
"source": "https://github.com/mathnotes-app/mobile-ink",
"commit": "fbb8017594896ebcaf0f6ddf6b7632c7250a2a1d",
"pull_request": "https://github.com/mathnotes-app/mobile-ink/pull/61",
"sha256": "49de52afa68fbd478283b456cf63d3cb34af07272cc612a22cad03aecc337010"
"commit": "8316b8c8931b933b01e6ee1e34141ae47264d9d3",
"sha256": "041f2c970c80e3ea074d424735530cb7d8de7765f3128dd3261da3f90137a5e6",
"pull_request": "https://github.com/mathnotes-app/mobile-ink/pull/62"
}
Loading