Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Nextcloud GmbH
# SPDX-License-Identifier: GPL-3.0-or-later

version: 2
updates:
# Swift Package Manager dependencies (Alamofire, NextcloudKit, swift-markdown, …)
- package-ecosystem: "swift"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "fix(deps)"

# Keep the GitHub Actions used by our workflows up to date
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
commit-message:
prefix: "ci"
File renamed without changes.
100 changes: 100 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: Nextcloud GmbH
# SPDX-License-Identifier: GPL-3.0-or-later

name: Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: macos-15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Select latest installed Xcode
run: |
# Pick the newest Xcode present on the runner instead of hard-coding a
# path, so the job keeps working as runner images change.
LATEST=$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort -V | tail -1)
LATEST=${LATEST:-/Applications/Xcode.app}
echo "Selecting $LATEST"
sudo xcode-select --switch "$LATEST"

- name: Show versions
run: |
xcodebuild -version
swift --version

- name: Resolve Swift package dependencies
run: |
xcodebuild -resolvePackageDependencies \
-project iOCNotes.xcodeproj \
-scheme iOCNotes

- name: Pick an iOS simulator
id: sim
run: |
# Choose the newest available iPhone simulator so we don't hard-code a
# device name that may change between Xcode releases.
cat > /tmp/pick_sim.py <<'PY'
import json, sys
devices = json.load(sys.stdin)["devices"]
cands = [d for rt, ds in devices.items() if "iOS" in rt
for d in ds if d.get("isAvailable") and "iPhone" in d["name"]]
print(cands[-1]["udid"] if cands else "")
PY
UDID=$(xcrun simctl list devices available --json | python3 /tmp/pick_sim.py)
if [ -z "$UDID" ]; then
echo "No iOS simulator available" >&2
xcrun simctl list devices available
exit 1
fi
echo "Using simulator $UDID"
echo "udid=$UDID" >> "$GITHUB_OUTPUT"

- name: Run unit tests
run: |
xcodebuild test \
-project iOCNotes.xcodeproj \
-scheme iOCNotes \
-destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \
-resultBundlePath TestResults.xcresult \
-skipPackagePluginValidation \
-parallel-testing-enabled NO \
CODE_SIGNING_ALLOWED=NO

- name: Show failure details
if: failure()
run: |
# Surface the actual test/crash reason from the result bundle, since a
# host-app launch failure prints nothing useful to the build log.
echo "===== test results summary ====="
xcrun xcresulttool get test-results summary \
--path TestResults.xcresult || true
echo "===== crash backtraces ====="
xcrun xcresulttool export diagnostics \
--path TestResults.xcresult \
--output-path _diagnostics 2>/dev/null || true
find _diagnostics TestResults.xcresult \
\( -name "*.crash" -o -name "*.ips" \) 2>/dev/null \
| while read -r f; do echo "--- $f ---"; sed -n '1,150p' "$f" || true; done

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: TestResults
path: TestResults.xcresult
if-no-files-found: ignore
64 changes: 28 additions & 36 deletions IOCNotesUnitTests/MarkdownTextStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ struct MarkdownTextStorageTests {

private func applyText(_ text: String) -> NSAttributedString {
let textStorage = MarkdownTextStorage()
let attributedString = NSAttributedString(string: text)
textStorage.setAttributedString(attributedString)

// Manually trigger formatting since we're not in a text view
let range = NSRange(location: 0, length: text.count)
textStorage.edited([.editedCharacters, .editedAttributes], range: range, changeInLength: 0)
textStorage.processEditing()


// Drive the storage the way a UITextView does: insert the text through
// `replaceCharacters` inside a begin/end editing group. This sets a valid
// `editedRange` and lets `processEditing` run exactly as it does in the
// app. Calling `edited`/`processEditing` by hand instead leaves the
// edited range in the `{NSNotFound, …}` sentinel state, which crashes
// NSTextStorage's attribute fixing.
textStorage.beginEditing()
textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: text)
textStorage.endEditing()

return NSAttributedString(attributedString: textStorage)
}

Expand Down Expand Up @@ -48,6 +51,14 @@ struct MarkdownTextStorageTests {
guard location < attributedString.length else { return nil }
return attributedString.attribute(.strikethroughStyle, at: location, effectiveRange: nil) as? Int
}

private func isBodyMonospacedFont(_ font: UIFont?) -> Bool {
guard let font else { return false }
let bodyFont = UIFont.preferredFont(forTextStyle: .body)
let expectedFont = UIFont(style: .body, design: .monospaced)
?? UIFont.monospacedSystemFont(ofSize: bodyFont.pointSize, weight: .regular)
return font.fontName == expectedFont.fontName && font.pointSize == expectedFont.pointSize
}

// MARK: - Header Tests

Expand All @@ -65,14 +76,13 @@ struct MarkdownTextStorageTests {

#expect(font != nil, "Font should not be nil for: \(text)")

// Check that font size matches expected style (larger than body)
// Check that the configured text style is applied.
let expectedFont = UIFont.preferredFont(forTextStyle: expectedStyle)
let bodyFont = UIFont.preferredFont(forTextStyle: .body)

#expect(font!.pointSize >= expectedFont.pointSize * 0.9,
"Header font size should be appropriate for: \(text)")
#expect(font!.pointSize > bodyFont.pointSize,
"Header font should be larger than body font for: \(text)")
#expect(font!.pointSize == expectedFont.pointSize,
"Header font should match its configured text style for: \(text)")
}

@Test("Header hashtag fading")
Expand Down Expand Up @@ -120,10 +130,7 @@ struct MarkdownTextStorageTests {
let codeHeaderRange = nsText.range(of: "# This is not a header")
if codeHeaderRange.location != NSNotFound {
let font = getFontAt(codeHeaderRange.location, in: result)
#expect(font?.familyName.contains("Menlo") == true ||
font?.familyName.contains("Monaco") == true ||
font?.familyName.contains("Courier") == true ||
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true,
#expect(isBodyMonospacedFont(font),
"Header inside code block should use monospaced font")
}
}
Expand Down Expand Up @@ -284,10 +291,7 @@ struct MarkdownTextStorageTests {
let font = getFontAt(startIndex, in: result)
let backgroundColor = getBackgroundColorAt(startIndex, in: result)

#expect(font?.familyName.contains("Menlo") == true ||
font?.familyName.contains("Monaco") == true ||
font?.familyName.contains("Courier") == true ||
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true,
#expect(isBodyMonospacedFont(font),
"Inline code should use monospaced font")
#expect(backgroundColor != nil, "Inline code should have background color")
}
Expand Down Expand Up @@ -323,10 +327,7 @@ struct MarkdownTextStorageTests {
let font = getFontAt(currentIndex, in: result)
let backgroundColor = getBackgroundColorAt(currentIndex, in: result)

#expect(font?.familyName.contains("Menlo") == true ||
font?.familyName.contains("Monaco") == true ||
font?.familyName.contains("Courier") == true ||
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true,
#expect(isBodyMonospacedFont(font),
"Code block should use monospaced font for line: \(line)")
#expect(backgroundColor != nil, "Code block should have background color")
}
Expand Down Expand Up @@ -361,10 +362,7 @@ struct MarkdownTextStorageTests {
let font = getFontAt(notBoldRange.location, in: result)

// Should be monospaced
#expect(font?.familyName.contains("Menlo") == true ||
font?.familyName.contains("Monaco") == true ||
font?.familyName.contains("Courier") == true ||
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true,
#expect(isBodyMonospacedFont(font),
"Code should be monospaced")

// Should not have special formatting
Expand All @@ -376,10 +374,7 @@ struct MarkdownTextStorageTests {
let font = getFontAt(notHeaderRange.location, in: result)

// Should be monospaced, not larger header font
#expect(font?.familyName.contains("Menlo") == true ||
font?.familyName.contains("Monaco") == true ||
font?.familyName.contains("Courier") == true ||
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true,
#expect(isBodyMonospacedFont(font),
"Code should be monospaced, not header styled")
}
}
Expand Down Expand Up @@ -495,10 +490,7 @@ struct MarkdownTextStorageTests {
let codeTextRange = nsText.range(of: "**Not bold**")
if codeTextRange.location != NSNotFound {
let font = getFontAt(codeTextRange.location, in: result)
#expect(font?.familyName.contains("Menlo") == true ||
font?.familyName.contains("Monaco") == true ||
font?.familyName.contains("Courier") == true ||
font?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) == true,
#expect(isBodyMonospacedFont(font),
"Code block should use monospaced font")
#expect(!font!.fontDescriptor.symbolicTraits.contains(.traitBold),
"Text in code block should not be bold")
Expand Down
15 changes: 11 additions & 4 deletions IOCNotesUnitTests/NotesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
// SPDX-FileCopyrightText: 2019 Peter Hedlund
// SPDX-License-Identifier: GPL-3.0-or-later

import Testing
import Dispatch
import Foundation
import Testing
@testable import iOCNotes

@Suite("Notes Management Tests")
@Suite(
"Notes Management Tests",
.enabled(
if: ProcessInfo.processInfo.environment["NOTES_INTEGRATION_TESTS"] == "1",
"Requires the local Nextcloud test server"
)
)
class NotesTests {
var originalServer: String = ""
var originalUser: String = ""
Expand All @@ -20,8 +27,8 @@ class NotesTests {

// Set test values
KeychainHelper.server = "http://localhost:8080"
KeychainHelper.username = "cloudnotes"
KeychainHelper.password = "cloudnotes"
KeychainHelper.username = "nonotes"
KeychainHelper.password = "password"

// Clear database
Note.reset()
Expand Down
9 changes: 8 additions & 1 deletion Server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ NEXTCLOUD_TRUSTED_DOMAINS='192.168.178.*' ./Server/Run.sh
You only need this one as long as you do not delete the container again.
Until then you can start or stop it with Docker Desktop.

The network-backed tests are opt-in so regular unit-test and CI runs do not
depend on this server. After provisioning it, include those tests with:

```sh
NOTES_INTEGRATION_TESTS=1 xcodebuild test -scheme iOCNotes -destination 'platform=iOS Simulator,name=iPhone 16'
```

To quickly get rid of the container and delete all its data:

```sh
Expand All @@ -45,4 +52,4 @@ docker rm --force --volumes nextcloud-notes-test-server
### A Lot Of Notes

In `Notes/A Lot Of/` there is a shell script to quickly generate about a thousand random markdown files.
To avoid bloating the repository with random test data, this has been excluded intentionally and needs to be generated on demand.
To avoid bloating the repository with random test data, this has been excluded intentionally and needs to be generated on demand.
10 changes: 0 additions & 10 deletions iOCNotes.xcodeproj/xcshareddata/xcschemes/iOCNotes.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0CFE5561888A7BD00165839"
BuildableName = "iOCNotesTests.xctest"
BlueprintName = "iOCNotesTests"
ReferencedContainer = "container:iOCNotes.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
Expand Down
Loading