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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ private struct PluginDirectoryRemoteConstants {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "YYYY-MM-dd h:mma z"
formatter.dateFormat = "yyyy-MM-dd h:mma z"
return formatter
}()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Testing
import WordPressKit
@testable import WordPress

struct PluginDirectoryEntryLookupTests {

/// The directory entries dictionary in PluginStoreState is keyed by slug.
/// PluginViewModel must use `plugin.state.slug` (not `plugin.id`) to look
/// up entries, because `id` and `slug` are different values.
///
/// Example: id = "jetpack/jetpack.php", slug = "jetpack"
@Test
func pluginIdDiffersFromSlug() {
let state = PluginState(
id: "jetpack/jetpack.php",
slug: "jetpack",
active: true,
name: "Jetpack",
author: "Automattic",
version: "5.5",
updateState: .updated,
autoupdate: false,
automanaged: false,
url: nil,
settingsURL: nil
)
let plugin = Plugin(state: state, directoryEntry: nil)

#expect(plugin.id != plugin.state.slug,
"plugin.id and plugin.state.slug should differ")
#expect(plugin.id == "jetpack/jetpack.php")
#expect(plugin.state.slug == "jetpack")
}

@Test
func directoryEntryLookupBySlugSucceeds() {
let entry = makeDirectoryEntry(slug: "jetpack")

// Simulate the directoryEntries dictionary (keyed by slug)
var directoryEntries = [String: PluginDirectoryEntryState]()
directoryEntries["jetpack"] = .present(entry)

// Looking up by slug succeeds
#expect(directoryEntries["jetpack"]?.entry != nil)
}

@Test
func directoryEntryLookupByIdFails() {
let entry = makeDirectoryEntry(slug: "jetpack")

// Simulate the directoryEntries dictionary (keyed by slug)
var directoryEntries = [String: PluginDirectoryEntryState]()
directoryEntries["jetpack"] = .present(entry)

// Looking up by plugin.id (the bug) fails because id != slug
let pluginId = "jetpack/jetpack.php"
#expect(directoryEntries[pluginId]?.entry == nil,
"Looking up by plugin.id should fail since entries are keyed by slug")
}

@Test
func pluginStoreGetPluginDirectoryEntryUsesSlugKey() {
let store = PluginStore()
let entry = makeDirectoryEntry(slug: "jetpack")

// Populate the store's directoryEntries via its state
store.state.directoryEntries["jetpack"] = .present(entry)

// Lookup by slug succeeds
#expect(store.getPluginDirectoryEntry(slug: "jetpack") != nil)

// Lookup by id fails
#expect(store.getPluginDirectoryEntry(slug: "jetpack/jetpack.php") == nil)
}

// MARK: - Helpers

private func makeDirectoryEntry(slug: String) -> PluginDirectoryEntry {
let json = """
{
"name": "Test Plugin",
"slug": "\(slug)",
"version": "1.0",
"author": "Test",
"rating": 80,
"icons": {},
"sections": {}
}
""".data(using: .utf8)!

return try! JSONDecoder().decode(PluginDirectoryEntry.self, from: json)
}
}
48 changes: 48 additions & 0 deletions Tests/KeystoneTests/Tests/Features/Stats/DonutChartViewTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Testing
import UIKit
@testable import WordPress

struct DonutChartViewTests {

@Test
func configureWithEmptySegmentsDoesNotCrash() {
let chartView = DonutChartView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))

// When totalCount > 0 but all segment values are 0, normalizedSegments()
// filters them all out, leaving an empty array. Previously this caused
// a crash on `0..<segments.count - 1` (range 0..<-1).
chartView.configure(
title: "Test",
totalCount: 100,
segments: [
DonutChartView.Segment(title: "A", value: 0, color: .blue),
DonutChartView.Segment(title: "B", value: 0, color: .red)
]
)
}

@Test
func configureWithValidSegmentsDoesNotCrash() {
let chartView = DonutChartView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))

chartView.configure(
title: "Test",
totalCount: 100,
segments: [
DonutChartView.Segment(title: "A", value: 60, color: .blue),
DonutChartView.Segment(title: "B", value: 40, color: .red)
]
)
}

@Test
func configureWithZeroTotalCountDoesNotCrash() {
let chartView = DonutChartView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))

chartView.configure(
title: "Test",
totalCount: 0,
segments: []
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,12 @@ import WordPressKit

class PluginDirectoryEntryStateTests: XCTestCase {

static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "YYYY-MM-dd h:mma z"
return formatter
}()

static let jetpackEntry: PluginDirectoryEntry = {
let json = Bundle(for: PluginDirectoryEntryStateTests.self).url(forResource: "plugin-directory-jetpack", withExtension: "json")!
let data = try! Data(contentsOf: json)

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(PluginDirectoryEntryStateTests.dateFormatter)

return try! jsonDecoder.decode(PluginDirectoryEntry.self, from: data)
let endpoint = PluginDirectoryGetInformationEndpoint(slug: "jetpack")
return try! endpoint.parseResponse(data: data)
}()

func testMoreSpecificDirectoryEntryStateWins() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,31 @@ class PluginDirectoryTests: XCTestCase {
XCTAssertTrue(lhs == rhs)
}

func testDateParsingNearYearBoundary() throws {
// Verify that dates near the year boundary are parsed with the correct calendar year.
// Previously the format used "YYYY" (week-year) instead of "yyyy" (calendar year),
// which caused Dec 31 dates to be parsed as the following year.
let json = """
{
"name": "Test Plugin",
"slug": "test-plugin",
"version": "1.0",
"last_updated": "2024-12-31 8:00pm GMT",
"author": "Test Author",
"rating": 80,
"icons": {},
"sections": {}
}
""".data(using: .utf8)!

let endpoint = PluginDirectoryGetInformationEndpoint(slug: "test-plugin")
let plugin = try endpoint.parseResponse(data: json)

let calendar = Calendar(identifier: .gregorian)
let year = calendar.component(.year, from: plugin.lastUpdated!)
XCTAssertEqual(year, 2024, "Date near year boundary should parse as 2024, not 2025 (week-year)")
}

func testUnconventionalPluginSlug() async throws {
let data = try MockPluginDirectoryProvider.getPluginDirectoryMockData(with: "plugin-directory-rename-xml-rpc", sender: type(of: self))
stub(condition: isHost("api.wordpress.org")) { _ in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ class GutenbergViewController: UIViewController, PostEditor, PublishingEditor {

let borderBottom = UIView()
borderBottom.backgroundColor = UIColor(cgColor: borderColor)
borderBottom.frame = CGRect(x: 0, y: navigationController?.navigationBar.frame.size.height ?? 0 - borderWidth, width: navigationController?.navigationBar.frame.size.width ?? 0, height: borderWidth)
borderBottom.frame = CGRect(x: 0, y: (navigationController?.navigationBar.frame.size.height ?? 0) - borderWidth, width: navigationController?.navigationBar.frame.size.width ?? 0, height: borderWidth)
borderBottom.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
navigationController?.navigationBar.addSubview(borderBottom)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class PluginViewModel: Observable {
// Self hosted non-Jetpack plugins may not have the directory entry set
// attempt to find one for this plugin
if updatedPlugin.directoryEntry == nil {
updatedPlugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.id)
updatedPlugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.state.slug)
}

self.state = .plugin(updatedPlugin)
Expand All @@ -88,7 +88,7 @@ class PluginViewModel: Observable {
}

if plugin.directoryEntry == nil {
plugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.id)
plugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.state.slug)
}

self?.state = .plugin(plugin)
Expand All @@ -114,7 +114,7 @@ class PluginViewModel: Observable {
let state: State
if var plugin = store.getPlugin(slug: slug, site: site) {
if plugin.directoryEntry == nil {
plugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.id)
plugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.state.slug)
}

state = .plugin(plugin)
Expand All @@ -141,7 +141,7 @@ class PluginViewModel: Observable {

if var plugin = self?.store.getPlugin(slug: entry.slug, site: site) {
if plugin.directoryEntry == nil {
plugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.id)
plugin.directoryEntry = store.getPluginDirectoryEntry(slug: plugin.state.slug)
}

self?.state = .plugin(plugin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class DonutChartView: UIView {
// Here we'll increase the size of small segments if necessary. We loop through segments.count times
// to ensure that after each adjustment the remaining segments are still an acceptable size.
var displaySegments = adjustedSegmentsForDisplay(segments)
for _ in 0..<segments.count - 1 {
for _ in 0..<max(segments.count, 1) - 1 {
displaySegments = adjustedSegmentsForDisplay(displaySegments)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ class StatsChartMarker: MarkerView {

func drawTopRightRect(context: CGContext, x: CGFloat, y: CGFloat, height: CGFloat, width: CGFloat) {
let arrowHeight = Constants.arrowSize.height
let arrowWidth = Constants.arrowSize.width

drawDot(context: context, xPosition: x + width - Constants.dotRadius, yPosition: y - Constants.dotRadius)

Expand All @@ -223,7 +224,7 @@ class StatsChartMarker: MarkerView {
context.addLine(to: CGPoint(x: x, y: y + arrowHeight + Constants.cornerRadius + Constants.dotRadius))
// Top left corner
context.addQuadCurve(to: CGPoint(x: x + Constants.cornerRadius, y: y + arrowHeight + Constants.dotRadius), control: CGPoint(x: x, y: y + arrowHeight + Constants.dotRadius))
context.addLine(to: CGPoint(x: x + width - arrowHeight / 2.0, y: y + arrowHeight + Constants.dotRadius))
context.addLine(to: CGPoint(x: x + width - arrowWidth / 2.0, y: y + arrowHeight + Constants.dotRadius))
context.addLine(to: CGPoint(x: x + width, y: y + Constants.dotRadius))
context.fillPath()
}
Expand Down